mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-08-10 00:52:16 +00:00
21
.github/workflows/CI.yml
vendored
21
.github/workflows/CI.yml
vendored
@@ -9,6 +9,10 @@ on:
|
||||
branches: [master, nightly]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
github_env:
|
||||
name: GitHub Env Debug
|
||||
@@ -185,7 +189,7 @@ jobs:
|
||||
elif [[ ${{ github.ref == 'refs/heads/nightly' }} ]]; then
|
||||
sub_version=".r${commit}"
|
||||
|
||||
echo "aur_publish=true" >> $GITHUB_ENV
|
||||
echo "aur_publish=false" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "This is a PR event"
|
||||
@@ -388,7 +392,8 @@ jobs:
|
||||
libboost-filesystem1.71-dev \
|
||||
libboost-log1.71-dev \
|
||||
libboost-regex1.71-dev \
|
||||
libboost-thread1.71-dev
|
||||
libboost-thread1.71-dev \
|
||||
libboost-program-options1.71-dev
|
||||
|
||||
# Install cmake
|
||||
wget https://cmake.org/files/v3.22/cmake-3.22.2-linux-x86_64.sh
|
||||
@@ -412,7 +417,8 @@ jobs:
|
||||
cmake \
|
||||
libboost-filesystem-dev \
|
||||
libboost-log-dev \
|
||||
libboost-thread-dev
|
||||
libboost-thread-dev \
|
||||
libboost-program-options-dev
|
||||
fi
|
||||
|
||||
sudo apt-get install -y \
|
||||
@@ -466,9 +472,7 @@ jobs:
|
||||
mkdir -p build
|
||||
mkdir -p artifacts
|
||||
|
||||
pushd "./src_assets/common/assets/web"
|
||||
npm install
|
||||
popd
|
||||
|
||||
cd build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release \
|
||||
@@ -588,9 +592,7 @@ jobs:
|
||||
|
||||
- name: Build MacOS
|
||||
run: |
|
||||
pushd "./src_assets/common/assets/web"
|
||||
npm install
|
||||
popd
|
||||
|
||||
mkdir build
|
||||
cd build
|
||||
@@ -888,12 +890,11 @@ jobs:
|
||||
mingw-w64-x86_64-openssl
|
||||
mingw-w64-x86_64-opus
|
||||
mingw-w64-x86_64-toolchain
|
||||
mingw-w64-x86_64-x265
|
||||
nasm
|
||||
wget
|
||||
yasm
|
||||
|
||||
- name: Install npm packages
|
||||
working-directory: src_assets/common/assets/web
|
||||
run: |
|
||||
npm install
|
||||
|
||||
@@ -906,7 +907,7 @@ jobs:
|
||||
-DSUNSHINE_ASSETS_DIR=assets \
|
||||
-G "MinGW Makefiles" \
|
||||
..
|
||||
mingw32-make -j2
|
||||
mingw32-make -j$(nproc)
|
||||
|
||||
- name: Package Windows
|
||||
shell: msys2 {0}
|
||||
|
||||
3
.github/workflows/auto-create-pr.yml
vendored
3
.github/workflows/auto-create-pr.yml
vendored
@@ -3,6 +3,9 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# This workflow creates a PR automatically when anything is merged/pushed into the `nightly` branch. The PR is created
|
||||
# against the `master` (default) branch.
|
||||
|
||||
name: Auto create PR
|
||||
|
||||
on:
|
||||
|
||||
11
.github/workflows/automerge.yml
vendored
11
.github/workflows/automerge.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# This workflow will, first, automatically approve PRs created by @LizardByte-bot. Then it will automerge relevant PRs.
|
||||
|
||||
name: Automerge PR
|
||||
|
||||
on:
|
||||
@@ -11,6 +13,10 @@ on:
|
||||
- opened
|
||||
- synchronize
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
autoapprove:
|
||||
if: >-
|
||||
@@ -40,9 +46,6 @@ jobs:
|
||||
if: startsWith(github.repository, 'LizardByte/')
|
||||
needs: [autoapprove]
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: automerge-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Automerging
|
||||
@@ -51,7 +54,7 @@ jobs:
|
||||
BASE_BRANCHES: nightly
|
||||
GITHUB_TOKEN: ${{ secrets.GH_BOT_TOKEN }}
|
||||
GITHUB_LOGIN: ${{ secrets.GH_BOT_NAME }}
|
||||
MERGE_LABELS: ""
|
||||
MERGE_LABELS: "!dependencies"
|
||||
MERGE_METHOD: "squash"
|
||||
MERGE_COMMIT_MESSAGE: "{pullRequest.title} (#{pullRequest.number})"
|
||||
MERGE_DELETE_BRANCH: true
|
||||
|
||||
15
.github/workflows/autoupdate-labeler.yml
vendored
15
.github/workflows/autoupdate-labeler.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Label PRs with `autoupdate` if various conditions are met, otherwise, remove the label.
|
||||
|
||||
name: Label PR autoupdate
|
||||
|
||||
on:
|
||||
@@ -10,11 +12,8 @@ on:
|
||||
types:
|
||||
- edited
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
pull_request_review:
|
||||
types:
|
||||
- edited
|
||||
- submitted
|
||||
|
||||
jobs:
|
||||
label_pr:
|
||||
@@ -40,12 +39,7 @@ jobs:
|
||||
steps.org_member.outputs.result == 'true' &&
|
||||
contains(github.event.pull_request.labels.*.name, 'autoupdate') == false &&
|
||||
contains(github.event.pull_request.body,
|
||||
fromJSON('"\n- [x] I want maintainers to keep my branch updated"')) == true &&
|
||||
(
|
||||
(github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
|
||||
(github.event_name == 'pull_request')
|
||||
)
|
||||
|
||||
fromJSON('"\n- [x] I want maintainers to keep my branch updated"')) == true
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GH_BOT_TOKEN }}
|
||||
@@ -60,7 +54,6 @@ jobs:
|
||||
- name: Unlabel autoupdate
|
||||
if: >-
|
||||
contains(github.event.pull_request.labels.*.name, 'autoupdate') &&
|
||||
github.event_name == 'pull_request' &&
|
||||
(
|
||||
(github.event.action == 'synchronize' && steps.org_member.outputs.result == 'false') ||
|
||||
(contains(github.event.pull_request.body,
|
||||
|
||||
6
.github/workflows/autoupdate.yml
vendored
6
.github/workflows/autoupdate.yml
vendored
@@ -7,8 +7,9 @@
|
||||
# - automerge
|
||||
# - autoupdate-labeler
|
||||
|
||||
# It uses GitHub Action that auto-updates pull requests branches, when changes are pushed to their destination branch.
|
||||
# It uses an action that auto-updates pull requests branches, when changes are pushed to their destination branch.
|
||||
# Auto-updating to the latest destination branch works only in the context of upstream repo and not forks.
|
||||
# Dependabot PRs are updated by an action that comments `@depdenabot rebase` on dependabot PRs.
|
||||
|
||||
name: autoupdate
|
||||
|
||||
@@ -36,8 +37,7 @@ jobs:
|
||||
dependabot-rebase:
|
||||
name: Dependabot Rebase
|
||||
if: >-
|
||||
startsWith(github.repository, 'LizardByte/') &&
|
||||
contains(github.event.pull_request.labels.*.name, 'central_dependency') == false
|
||||
startsWith(github.repository, 'LizardByte/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: rebase
|
||||
|
||||
8
.github/workflows/ci-docker.yml
vendored
8
.github/workflows/ci-docker.yml
vendored
@@ -3,6 +3,10 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# This workflow is intended to work with all our organization Docker projects. Docker platforms/architectures should be
|
||||
# listed in a file named `.docker_platforms`, comma separated list with no spaces. A readme named `DOCKER_README.md`
|
||||
# will be used to update the description on Docker hub.
|
||||
|
||||
name: CI Docker
|
||||
|
||||
on:
|
||||
@@ -13,6 +17,10 @@ on:
|
||||
branches: [master, nightly]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check_dockerfile:
|
||||
name: Check Dockerfile
|
||||
|
||||
60
.github/workflows/cpp-clang-format-lint.yml
vendored
60
.github/workflows/cpp-clang-format-lint.yml
vendored
@@ -1,60 +0,0 @@
|
||||
---
|
||||
# This action is centrally managed in https://github.com/<organization>/.github/
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
name: Clang Format Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, nightly]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
check_src:
|
||||
name: Check src
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check
|
||||
id: check
|
||||
run: |
|
||||
if [ -d "./src" ]
|
||||
then
|
||||
FOUND=true
|
||||
else
|
||||
FOUND=false
|
||||
fi
|
||||
|
||||
echo "src=${FOUND}" >> $GITHUB_OUTPUT
|
||||
|
||||
outputs:
|
||||
src: ${{ steps.check.outputs.src }}
|
||||
|
||||
lint:
|
||||
name: Clang Format Lint
|
||||
needs: [check_src]
|
||||
if: ${{ needs.check_src.outputs.src == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Clang format lint
|
||||
uses: DoozyX/clang-format-lint-action@v0.15
|
||||
with:
|
||||
source: './src'
|
||||
extensions: 'cpp,h,m,mm'
|
||||
clangFormatVersion: 13
|
||||
style: file
|
||||
inplace: false
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: clang-format-fixes
|
||||
path: src/
|
||||
84
.github/workflows/cpp-lint.yml
vendored
Normal file
84
.github/workflows/cpp-lint.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
# This action is centrally managed in https://github.com/<organization>/.github/
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Lint c++ source files and cmake files.
|
||||
|
||||
name: C++ Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master, nightly]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
clang-format:
|
||||
name: Clang Format Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Find cpp files
|
||||
id: cpp_files
|
||||
run: |
|
||||
cpp_files=$(find . -type f -iname "*.cpp" -o -iname "*.h" -o -iname "*.m" -o -iname "*.mm")
|
||||
|
||||
echo "found cpp files: ${cpp_files}"
|
||||
|
||||
# do not quote to keep this as a single line
|
||||
echo cpp_files=${cpp_files} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Clang format lint
|
||||
if: ${{ steps.cpp_files.outputs.cpp_files }}
|
||||
uses: DoozyX/clang-format-lint-action@v0.15
|
||||
with:
|
||||
source: ${{ steps.cpp_files.outputs.cpp_files }}
|
||||
extensions: 'cpp,h,m,mm'
|
||||
clangFormatVersion: 15
|
||||
style: file
|
||||
inplace: false
|
||||
|
||||
- name: Upload Artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: clang-format-fixes
|
||||
path: ${{ steps.cpp_files.outputs.cpp_files }}
|
||||
|
||||
cmake-lint:
|
||||
name: CMake Lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools cmakelang
|
||||
|
||||
- name: Find cmake files
|
||||
id: cmake_files
|
||||
run: |
|
||||
cmake_files=$(find . -type f -iname "CMakeLists.txt" -o -iname "*.cmake")
|
||||
|
||||
echo "found cmake files: ${cmake_files}"
|
||||
|
||||
# do not quote to keep this as a single line
|
||||
echo cmake_files=${cmake_files} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Test with cmake-lint
|
||||
run: |
|
||||
cmake-lint --line-width 120 --tab-size 4 ${{ steps.cmake_files.outputs.cmake_files }}
|
||||
6
.github/workflows/issues-stale.yml
vendored
6
.github/workflows/issues-stale.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Manage stale issues and PRs.
|
||||
|
||||
name: Stale Issues / PRs
|
||||
|
||||
on:
|
||||
@@ -16,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@v6
|
||||
uses: actions/stale@v7
|
||||
with:
|
||||
close-issue-message: >
|
||||
This issue was closed because it has been stalled for 10 days with no activity.
|
||||
@@ -38,7 +40,7 @@ jobs:
|
||||
repo-token: ${{ secrets.GH_BOT_TOKEN }}
|
||||
|
||||
- name: Invalid Template
|
||||
uses: actions/stale@v6
|
||||
uses: actions/stale@v7
|
||||
with:
|
||||
close-issue-message: >
|
||||
This issue was closed because the the template was not completed after 5 days.
|
||||
|
||||
2
.github/workflows/issues.yml
vendored
2
.github/workflows/issues.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Label and un-label actions using `../label-actions.yml`.
|
||||
|
||||
name: Issues
|
||||
|
||||
on:
|
||||
|
||||
4
.github/workflows/pull-requests.yml
vendored
4
.github/workflows/pull-requests.yml
vendored
@@ -3,12 +3,16 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Ensure PRs are made against `nightly` branch.
|
||||
|
||||
name: Pull Requests
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, edited, reopened]
|
||||
|
||||
# no concurrency for pull_request_target events
|
||||
|
||||
jobs:
|
||||
check-pull-request:
|
||||
name: Check Pull Request
|
||||
|
||||
9
.github/workflows/python-flake8.yml
vendored
9
.github/workflows/python-flake8.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Lint python files with flake8.
|
||||
|
||||
name: flake8
|
||||
|
||||
on:
|
||||
@@ -10,6 +12,10 @@ on:
|
||||
branches: [master, nightly]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
flake8:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,7 +30,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip setuptools flake8
|
||||
# pin flake8 before v6.0.0 due to removal of support for type comments (required for Python 2.7 type hints)
|
||||
python -m pip install --upgrade pip setuptools "flake8<6"
|
||||
|
||||
- name: Test with flake8
|
||||
run: |
|
||||
|
||||
2
.github/workflows/release-notifier.yml
vendored
2
.github/workflows/release-notifier.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Send release notification to various platforms.
|
||||
|
||||
name: Release Notifications
|
||||
|
||||
on:
|
||||
|
||||
6
.github/workflows/yaml-lint.yml
vendored
6
.github/workflows/yaml-lint.yml
vendored
@@ -3,6 +3,8 @@
|
||||
# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in
|
||||
# the above-mentioned repo.
|
||||
|
||||
# Lint yaml files.
|
||||
|
||||
name: yaml lint
|
||||
|
||||
on:
|
||||
@@ -10,6 +12,10 @@ on:
|
||||
branches: [master, nightly]
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
yaml-lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
26
.gitmodules
vendored
26
.gitmodules
vendored
@@ -1,34 +1,44 @@
|
||||
[submodule "moonlight-common-c"]
|
||||
[submodule "third-party/moonlight-common-c"]
|
||||
path = third-party/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
[submodule "Simple-Web-Server"]
|
||||
branch = master
|
||||
[submodule "third-party/Simple-Web-Server"]
|
||||
path = third-party/Simple-Web-Server
|
||||
url = https://github.com/loki-47-6F-64/Simple-Web-Server.git
|
||||
[submodule "ViGEmClient"]
|
||||
url = https://gitlab.com/eidheim/Simple-Web-Server.git
|
||||
branch = master
|
||||
[submodule "third-party/ViGEmClient"]
|
||||
path = third-party/ViGEmClient
|
||||
url = https://github.com/ViGEm/ViGEmClient
|
||||
branch = master
|
||||
[submodule "third-party/miniupnp"]
|
||||
path = third-party/miniupnp
|
||||
url = https://github.com/miniupnp/miniupnp
|
||||
branch = master
|
||||
[submodule "third-party/nv-codec-headers"]
|
||||
path = third-party/nv-codec-headers
|
||||
url = https://github.com/FFmpeg/nv-codec-headers
|
||||
branch = sdk/11.1
|
||||
[submodule "third-party/TPCircularBuffer"]
|
||||
path = third-party/TPCircularBuffer
|
||||
url = https://github.com/michaeltyson/TPCircularBuffer
|
||||
[submodule "ffmpeg-windows-x86_64"]
|
||||
branch = master
|
||||
[submodule "third-party/ffmpeg-windows-x86_64"]
|
||||
path = third-party/ffmpeg-windows-x86_64
|
||||
url = https://github.com/LizardByte/build-deps
|
||||
branch = ffmpeg-windows-x86_64
|
||||
[submodule "ffmpeg-macos-x86_64"]
|
||||
[submodule "third-party/ffmpeg-macos-x86_64"]
|
||||
path = third-party/ffmpeg-macos-x86_64
|
||||
url = https://github.com/LizardByte/build-deps
|
||||
branch = ffmpeg-macos-x86_64
|
||||
[submodule "ffmpeg-linux-x86_64"]
|
||||
[submodule "third-party/ffmpeg-linux-x86_64"]
|
||||
path = third-party/ffmpeg-linux-x86_64
|
||||
url = https://github.com/LizardByte/build-deps
|
||||
branch = ffmpeg-linux-x86_64
|
||||
[submodule "ffmpeg-linux-aarch64"]
|
||||
[submodule "third-party/ffmpeg-linux-aarch64"]
|
||||
path = third-party/ffmpeg-linux-aarch64
|
||||
url = https://github.com/LizardByte/build-deps
|
||||
branch = ffmpeg-linux-aarch64
|
||||
[submodule "third-party/ffmpeg-macos-aarch64"]
|
||||
path = third-party/ffmpeg-macos-aarch64
|
||||
url = https://github.com/LizardByte/build-deps
|
||||
branch = ffmpeg-macos-aarch64
|
||||
|
||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -1,5 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## [0.17.0] - 2023-01-08
|
||||
If you are running Sunshine as a service on Windows, we are strongly urging you to update to v0.17.0 as soon as
|
||||
possible. Older Windows versions of Sunshine had a security flaw in which the binary was located in a user-writable
|
||||
location which is problematic when running as a service or on a multi-user system. Additionally, when running Sunshine
|
||||
as a service, games and applications were launched as SYSTEM. This could lead to issues with save files and other game
|
||||
settings. In v0.17.0, games now run under your user account without elevated privileges.
|
||||
|
||||
### Breaking
|
||||
- (Apps) Removed automatic desktop entry (Re-add by adding an empty application named "Desktop" with no commands, "desktop.png" can be added as the image.)
|
||||
- (Windows) Improved user upgrade experience (Suggest to manually uninstall existing Sunshine version before this upgrade. Do NOT select to remove everything, if prompted. Make a backup of config files before uninstall.)
|
||||
- (Windows) Move config files to specific directory (files will be migrated automatically if using Windows installer)
|
||||
- (Dependencies) Fix npm path (breaking change for package maintainers)
|
||||
### Added
|
||||
- (macOS) Added initial support for arm64 on macOS through Macports portfile
|
||||
- (Input) Added support for foreign keyboard input
|
||||
- (Misc) Logs inside the WebUI and log to file
|
||||
- (UI/Windows) Added an Apply button to configuration page when running as a service
|
||||
- (Input/Windows) Enable Mouse Keys while streaming for systems with no physical mouse
|
||||
### Fixed
|
||||
- (Video) Improved capture performance
|
||||
- (Audio) Improved audio bitrate and quality handling
|
||||
- (Apps/Windows) Fixed PATH environment variable handling
|
||||
- (Apps/Windows) Use the proper environment variable for the Program Files (x86) folder
|
||||
- (Service/Windows) Fix SunshineSvc hanging if an error occurs during startup
|
||||
- (Service/Windows) Spawn Sunshine.exe in a job object, so it is terminated if SunshineSvc.exe dies
|
||||
- (Video) windows/vram: fix fringing in NV12 colour conversion
|
||||
- (Apps/Windows) Launch games under the correct user account
|
||||
- (Video) nvenc, amdvce: rework all user presets/options
|
||||
- (Network) Generate certificates with unique serial numbers
|
||||
- (Service/Windows) Graceful termination on shutdown, logoff, and service stop
|
||||
- (Apps/Windows) Fix launching apps when Sunshine is running as admin
|
||||
- (Misc) Remove/fix calls to std::abort()
|
||||
- (Misc) Remove prompt to press enter after Sunshine exits
|
||||
- (Misc) Make log priority consistent for execution messages
|
||||
- (Apps) Applications in Moonlight clients are now updated automatically after editing
|
||||
- (Video/Linux) Fix wayland capture on nvidia
|
||||
- (Audio) Fix 7.1 surround channel mapping
|
||||
- (Video) Fix NVENC profile values not applying
|
||||
- (Network) Fix origin_web_ui_allowed binding
|
||||
- (Service/Windows) Self terminate/restart service if process hangs for 10 seconds
|
||||
- (Input/Windows) Fix Windows masked cursor blending with GPU encoders
|
||||
- (Video) Color conversion fixes and BT.2020 support
|
||||
### Dependencies
|
||||
- Bump ffmpeg from 4.4 to 5.1
|
||||
- ffmpeg_patches: add amfenc delay/buffering fix
|
||||
- CBS moved to ffmpeg submodules
|
||||
- Migrate to upstream Simple-Web-Server submodule
|
||||
- Bump third-party/TPCircularBuffer from `bce9170` to `8833b3a`
|
||||
- Bump third-party/moonlight-common-c from `8169a31` to `ef9ad52`
|
||||
- Bump third-party/miniupnp from `6f848ae` to `207cf44`
|
||||
- Bump third-party/ViGEmClient from `f719a1d` to `9e842ba`
|
||||
- Bump bootstrap from 5.0.0 to 5.2.3
|
||||
- Bump @fortawesome/fontawesome-free from 6.2.0 to 6.2.1
|
||||
|
||||
## [0.16.0] - 2022-12-13
|
||||
### Added
|
||||
- Add cover finder
|
||||
@@ -61,7 +115,7 @@
|
||||
- (Documentation) Added Sphinx documentation available at https://sunshinestream.readthedocs.io/en/latest/
|
||||
- (Development) Initial support for Localization
|
||||
- (Linux) Add rpm package as release asset
|
||||
- (MacOS) Add Portfile as release asset
|
||||
- (macOS) Add Portfile as release asset
|
||||
- (Windows) Add DwmFlush() call to improve capture
|
||||
- (Windows) Add Windows installer
|
||||
### Fixed
|
||||
@@ -70,13 +124,13 @@
|
||||
- (Linux) Fixed rumble events causing game to freeze
|
||||
- (Linux) Improved Pulse/Pipewire compatibility
|
||||
- (Linux) Moved to single deb package
|
||||
- (MacOS) Fixed missing TPCircularBuffer submodule
|
||||
- (macOS) Fixed missing TPCircularBuffer submodule
|
||||
- (Stream) Properly catch exceptions in stream broadcast handlers
|
||||
- (Stream/Video) AVPacket fix
|
||||
|
||||
## [0.13.0] - 2022-02-27
|
||||
### Added
|
||||
- (MacOS) Initial support for MacOS (#40)
|
||||
- (macOS) Initial support for macOS (#40)
|
||||
|
||||
## [0.12.0] - 2022-02-13
|
||||
### Added
|
||||
@@ -193,3 +247,28 @@
|
||||
## [0.1.0] - 2020-01-27
|
||||
### Added
|
||||
- The first official release for Sunshine!
|
||||
|
||||
[0.1.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.1.0
|
||||
[0.1.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.1.1
|
||||
[0.2.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.2.0
|
||||
[0.3.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.3.0
|
||||
[0.3.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.3.1
|
||||
[0.4.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.4.0
|
||||
[0.5.0]: https://github.com/LizardByte/Sunshine/releases/tag/0.5.0
|
||||
[0.6.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.6.0
|
||||
[0.7.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.7.0
|
||||
[0.7.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.7.1
|
||||
[0.7.7]: https://github.com/LizardByte/Sunshine/releases/tag/v0.7.7
|
||||
[0.8.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.8.0
|
||||
[0.9.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.9.0
|
||||
[0.10.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.10.0
|
||||
[0.10.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.10.1
|
||||
[0.11.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.11.0
|
||||
[0.11.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.11.1
|
||||
[0.12.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.12.0
|
||||
[0.13.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.13.0
|
||||
[0.14.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.14.0
|
||||
[0.14.1]: https://github.com/LizardByte/Sunshine/releases/tag/v0.14.1
|
||||
[0.15.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.15.0
|
||||
[0.16.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.16.0
|
||||
[0.17.0]: https://github.com/LizardByte/Sunshine/releases/tag/v0.17.0
|
||||
|
||||
1020
CMakeLists.txt
1020
CMakeLists.txt
File diff suppressed because it is too large
Load Diff
@@ -71,13 +71,13 @@ port `47990` (e.g. `http://<host_ip>:47990`). The internal port must be `47990`,
|
||||
(e.g. `-p 8080:47990`). All the ports listed in the `docker run` and `docker-compose` examples are required.
|
||||
|
||||
|
||||
| Parameter | Function | Example Value | Required |
|
||||
|-----------------------------|---------------------------|--------------------|----------|
|
||||
| `-p <port>:47990` | Web UI Port | `47990` | True |
|
||||
| `-v <path to data>:/config` | Volume mapping | `/home/sunshine` | True |
|
||||
| `-e PUID=<uid>` | User ID | `1001` | False |
|
||||
| `-e PGID=<gid>` | Group ID | `1001` | False |
|
||||
| `-e TZ=<timezone>` | Lookup TZ value [here][1] | `America/New_York` | False |
|
||||
| Parameter | Function | Example Value | Required |
|
||||
|-----------------------------|----------------------|--------------------|----------|
|
||||
| `-p <port>:47990` | Web UI Port | `47990` | True |
|
||||
| `-v <path to data>:/config` | Volume mapping | `/home/sunshine` | True |
|
||||
| `-e PUID=<uid>` | User ID | `1001` | False |
|
||||
| `-e PGID=<gid>` | Group ID | `1001` | False |
|
||||
| `-e TZ=<timezone>` | Lookup [TZ value][1] | `America/New_York` | False |
|
||||
|
||||
[1]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN apt-get update -y \
|
||||
libboost-filesystem-dev=1.74.0* \
|
||||
libboost-log-dev=1.74.0* \
|
||||
libboost-thread-dev=1.74.0* \
|
||||
libboost-program-options-dev=1.74.0* \
|
||||
libcap-dev=1:2.44* \
|
||||
libcurl4-openssl-dev=7.81.0* \
|
||||
libdrm-dev=2.4.110* \
|
||||
@@ -43,7 +44,6 @@ WORKDIR /root/sunshine-build/
|
||||
COPY . .
|
||||
|
||||
# setup npm and dependencies
|
||||
WORKDIR /root/sunshine-build/src_assets/common/assets/web
|
||||
RUN npm install
|
||||
|
||||
# setup build directory
|
||||
|
||||
97
README.rst
97
README.rst
@@ -4,34 +4,91 @@ LizardByte has the full documentation hosted on `Read the Docs <https://sunshine
|
||||
|
||||
About
|
||||
-----
|
||||
Sunshine is a Game stream host for Moonlight.
|
||||
Sunshine is a self hosted, low latency, cloud gaming solution with support for AMD, Intel, and Nvidia gpus.
|
||||
It is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
Connect to Sunshine from any Moonlight client, available for nearly any device imaginable.
|
||||
Sunshine is a self-hosted game stream host for Moonlight.
|
||||
Offering low latency, cloud gaming server capabilities with support for AMD, Intel, and Nvidia GPUs for hardware
|
||||
encoding. Software encoding is also available. You can connect to Sunshine from any Moonlight client on a variety of
|
||||
devices. A web UI is provided to allow configuration, and client pairing, from your favorite web browser. Pair from
|
||||
the local server or any mobile device.
|
||||
|
||||
These are the advantages of Sunshine over GeForce Experience.
|
||||
System Requirements
|
||||
-------------------
|
||||
|
||||
- FOSS (Free and Open Source Software)
|
||||
- Multi-platform
|
||||
.. warning:: This table is a work in progress. Do not purchase hardware based on this.
|
||||
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
**Minimum Requirements**
|
||||
|
||||
- Pair over web ui
|
||||
- Supports AMD, Intel, and Nvidia GPUs for encoding
|
||||
- Supports software encoding
|
||||
- Supports streaming to multiple clients
|
||||
- Web UI for configuration
|
||||
+------------+------------------------------------------------------------+
|
||||
| GPU | AMD: VCE 1.0 or higher, see `obs-amd hardware support`_ |
|
||||
| +------------------------------------------------------------+
|
||||
| | Intel: VAAPI-compatible, see: `VAAPI hardware support`_ |
|
||||
| +------------------------------------------------------------+
|
||||
| | Nvidia: NVENC enabled cards, see `nvenc support matrix`_ |
|
||||
+------------+------------------------------------------------------------+
|
||||
| CPU | AMD: Ryzen 3 or higher |
|
||||
| +------------------------------------------------------------+
|
||||
| | Intel: Core i3 or higher |
|
||||
+------------+------------------------------------------------------------+
|
||||
| RAM | 4GB or more |
|
||||
+------------+------------------------------------------------------------+
|
||||
| OS | Windows: 10+ (Windows Server not supported) |
|
||||
| +------------------------------------------------------------+
|
||||
| | macOS: 11.7+ |
|
||||
| +------------------------------------------------------------+
|
||||
| | Linux/Debian: 11 (bullseye) |
|
||||
| +------------------------------------------------------------+
|
||||
| | Linux/Fedora: 36+ |
|
||||
| +------------------------------------------------------------+
|
||||
| | Linux/Ubuntu: 20.04+ (focal) |
|
||||
+------------+------------------------------------------------------------+
|
||||
| Network | Host: 5GHz, 802.11ac |
|
||||
| +------------------------------------------------------------+
|
||||
| | Client: 5GHz, 802.11ac |
|
||||
+------------+------------------------------------------------------------+
|
||||
|
||||
**4k Suggestions**
|
||||
|
||||
+------------+------------------------------------------------------------+
|
||||
| GPU | AMD: Video Coding Engine 3.1 or higher |
|
||||
| +------------------------------------------------------------+
|
||||
| | Intel: HD Graphics 510 or higher |
|
||||
| +------------------------------------------------------------+
|
||||
| | Nvidia: GeForce GTX 1080 or higher |
|
||||
+------------+------------------------------------------------------------+
|
||||
| CPU | AMD: Ryzen 5 or higher |
|
||||
| +------------------------------------------------------------+
|
||||
| | Intel: Core i5 or higher |
|
||||
+------------+------------------------------------------------------------+
|
||||
| Network | Host: CAT5e ethernet or better |
|
||||
| +------------------------------------------------------------+
|
||||
| | Client: CAT5e ethernet or better |
|
||||
+------------+------------------------------------------------------------+
|
||||
|
||||
**HDR Suggestions**
|
||||
|
||||
+------------+------------------------------------------------------------+
|
||||
| GPU | AMD: Video Coding Engine 3.4 or higher |
|
||||
| +------------------------------------------------------------+
|
||||
| | Intel: UHD Graphics 730 or higher |
|
||||
| +------------------------------------------------------------+
|
||||
| | Nvidia: Pascal-based GPU (GTX 10-series) or higher |
|
||||
+------------+------------------------------------------------------------+
|
||||
| CPU | AMD: todo |
|
||||
| +------------------------------------------------------------+
|
||||
| | Intel: todo |
|
||||
+------------+------------------------------------------------------------+
|
||||
| Network | Host: CAT5e ethernet or better |
|
||||
| +------------------------------------------------------------+
|
||||
| | Client: CAT5e ethernet or better |
|
||||
+------------+------------------------------------------------------------+
|
||||
|
||||
Integrations
|
||||
------------
|
||||
|
||||
.. image:: https://img.shields.io/github/workflow/status/lizardbyte/sunshine/CI/master?label=CI%20build&logo=github&style=for-the-badge
|
||||
.. image:: https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/CI.yml.svg?branch=master&label=CI%20build&logo=github&style=for-the-badge
|
||||
:alt: GitHub Workflow Status (CI)
|
||||
:target: https://github.com/LizardByte/Sunshine/actions/workflows/CI.yml?query=branch%3Amaster
|
||||
|
||||
.. image:: https://img.shields.io/github/workflow/status/lizardbyte/sunshine/localize/nightly?label=localize%20build&logo=github&style=for-the-badge
|
||||
.. image:: https://img.shields.io/github/actions/workflow/status/lizardbyte/sunshine/localize.yml.svg?branch=nightly&label=localize%20build&logo=github&style=for-the-badge
|
||||
:alt: GitHub Workflow Status (localize)
|
||||
:target: https://github.com/LizardByte/Sunshine/actions/workflows/localize.yml?query=branch%3Anightly
|
||||
|
||||
@@ -44,7 +101,7 @@ Integrations
|
||||
:target: https://crowdin.com/project/sunshinestream
|
||||
|
||||
Support
|
||||
---------
|
||||
-------
|
||||
|
||||
Our support methods are listed in our
|
||||
`LizardByte Docs <https://lizardbyte.readthedocs.io/en/latest/about/support.html>`_.
|
||||
@@ -69,3 +126,7 @@ Stats
|
||||
.. image:: https://img.shields.io/badge/dynamic/json?color=blue&label=AUR&style=for-the-badge&query=$.results.0.NumVotes&url=https%3A%2F%2Fapp.lizardbyte.dev%2Funo%2Faur%2Fsunshine.json&logo=archlinux
|
||||
:alt: AUR votes
|
||||
:target: https://aur.archlinux.org/packages/sunshine
|
||||
|
||||
.. _nvenc support matrix: https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
|
||||
.. _obs-amd hardware support: https://github.com/obsproject/obs-amd-encoder/wiki/Hardware-Support
|
||||
.. _VAAPI hardware support: https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
# - Try to find FFMPEG
|
||||
# Once done this will define
|
||||
# FFMPEG_FOUND - System has FFMPEG
|
||||
# FFMPEG_INCLUDE_DIRS - The FFMPEG include directories
|
||||
# FFMPEG_LIBRARIES - The libraries needed to use FFMPEG
|
||||
# FFMPEG_LIBRARY_DIRS - The directory to find FFMPEG libraries
|
||||
#
|
||||
# written by Roy Shilkrot 2013 http://www.morethantechnical.com/
|
||||
#
|
||||
|
||||
find_package(PkgConfig)
|
||||
|
||||
|
||||
MACRO(FFMPEG_FIND varname shortname headername)
|
||||
|
||||
IF(NOT WIN32)
|
||||
PKG_CHECK_MODULES(PC_${varname} ${shortname})
|
||||
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}"
|
||||
HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
ELSE()
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}")
|
||||
ENDIF()
|
||||
|
||||
IF(${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
message(STATUS "look for newer strcture")
|
||||
IF(NOT WIN32)
|
||||
PKG_CHECK_MODULES(PC_${varname} "lib${shortname}")
|
||||
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}"
|
||||
HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS}
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
ELSE()
|
||||
FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}")
|
||||
IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
#Desperate times call for desperate measures
|
||||
MESSAGE(STATUS "globbing...")
|
||||
FILE(GLOB_RECURSE ${varname}_INCLUDE_DIR "/ffmpeg*/${headername}")
|
||||
MESSAGE(STATUS "found: ${${varname}_INCLUDE_DIR}")
|
||||
IF(${varname}_INCLUDE_DIR)
|
||||
GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH)
|
||||
GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH)
|
||||
ELSE()
|
||||
SET(${varname}_INCLUDE_DIR "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
|
||||
|
||||
IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
|
||||
MESSAGE(STATUS "Can't find includes for ${shortname}...")
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Found ${shortname} include dirs: ${${varname}_INCLUDE_DIR}")
|
||||
|
||||
#GET_DIRECTORY_PROPERTY(FFMPEG_PARENT DIRECTORY ${${varname}_INCLUDE_DIR} PARENT_DIRECTORY)
|
||||
GET_FILENAME_COMPONENT(FFMPEG_PARENT ${${varname}_INCLUDE_DIR} PATH)
|
||||
MESSAGE(STATUS "Using FFMpeg dir parent as hint: ${FFMPEG_PARENT}")
|
||||
|
||||
IF(NOT WIN32)
|
||||
FIND_LIBRARY(${varname}_LIBRARIES NAMES ${shortname}
|
||||
HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT})
|
||||
ELSE()
|
||||
FIND_PATH(${varname}_LIBRARIES "${shortname}.dll.a" HINTS ${FFMPEG_PARENT})
|
||||
# FILE(GLOB_RECURSE ${varname}_LIBRARIES "${FFMPEG_PARENT}/*${shortname}.lib")
|
||||
# GLOBing is very bad... but windows sux, this is the only thing that works
|
||||
ENDIF()
|
||||
|
||||
IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND")
|
||||
MESSAGE(STATUS "look for newer structure for library")
|
||||
FIND_LIBRARY(${varname}_LIBRARIES NAMES lib${shortname}
|
||||
HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT})
|
||||
ENDIF()
|
||||
|
||||
|
||||
IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND")
|
||||
MESSAGE(STATUS "Can't find lib for ${shortname}...")
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Found ${shortname} libs: ${${varname}_LIBRARIES}")
|
||||
ENDIF()
|
||||
|
||||
|
||||
IF(NOT ${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND"
|
||||
AND NOT ${varname}_LIBRARIES STREQUAL ${varname}_LIBRARIES-NOTFOUND)
|
||||
|
||||
MESSAGE(STATUS "found ${shortname}: include ${${varname}_INCLUDE_DIR} lib ${${varname}_LIBRARIES}")
|
||||
SET(FFMPEG_${varname}_FOUND 1)
|
||||
SET(FFMPEG_${varname}_INCLUDE_DIRS ${${varname}_INCLUDE_DIR})
|
||||
SET(FFMPEG_${varname}_LIBS ${${varname}_LIBRARIES})
|
||||
ELSE()
|
||||
MESSAGE(STATUS "Can't find ${shortname}")
|
||||
ENDIF()
|
||||
|
||||
ENDIF()
|
||||
|
||||
ENDMACRO(FFMPEG_FIND)
|
||||
|
||||
FFMPEG_FIND(LIBAVFORMAT avformat avformat.h)
|
||||
FFMPEG_FIND(LIBAVDEVICE avdevice avdevice.h)
|
||||
FFMPEG_FIND(LIBAVCODEC avcodec avcodec.h)
|
||||
FFMPEG_FIND(LIBAVUTIL avutil avutil.h)
|
||||
FFMPEG_FIND(LIBSWSCALE swscale swscale.h)
|
||||
|
||||
SET(FFMPEG_FOUND "NO")
|
||||
IF (FFMPEG_LIBAVFORMAT_FOUND AND
|
||||
FFMPEG_LIBAVDEVICE_FOUND AND
|
||||
FFMPEG_LIBAVCODEC_FOUND AND
|
||||
FFMPEG_LIBAVUTIL_FOUND AND
|
||||
FFMPEG_LIBSWSCALE_FOUND
|
||||
)
|
||||
|
||||
|
||||
SET(FFMPEG_FOUND "YES")
|
||||
|
||||
SET(FFMPEG_INCLUDE_DIRS ${FFMPEG_LIBAVFORMAT_INCLUDE_DIRS})
|
||||
|
||||
SET(FFMPEG_LIBRARY_DIRS ${FFMPEG_LIBAVFORMAT_LIBRARY_DIRS})
|
||||
|
||||
SET(FFMPEG_LIBRARIES
|
||||
${FFMPEG_LIBAVFORMAT_LIBS}
|
||||
${FFMPEG_LIBAVDEVICE_LIBS}
|
||||
${FFMPEG_LIBAVCODEC_LIBS}
|
||||
${FFMPEG_LIBAVUTIL_LIBS}
|
||||
${FFMPEG_LIBSWSCALE_LIBS}
|
||||
)
|
||||
|
||||
ELSE ()
|
||||
|
||||
MESSAGE(STATUS "Could not find FFMPEG")
|
||||
|
||||
ENDIF()
|
||||
|
||||
message(STATUS ${FFMPEG_LIBRARIES} ${FFMPEG_LIBAVFORMAT_LIBRARIES})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
# handle the QUIETLY and REQUIRED arguments and set FFMPEG_FOUND to TRUE
|
||||
# if all listed variables are TRUE
|
||||
find_package_handle_standard_args(FFMPEG DEFAULT_MSG
|
||||
FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS)
|
||||
|
||||
mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARY_DIRS FFMPEG_LIBRARIES)
|
||||
@@ -18,4 +18,4 @@ find_library(LIBDRM_LIBRARIES NAMES libdrm.so PATHS ${PC_LIBDRM_LIBDIR} ${PC_LIB
|
||||
mark_as_advanced(LIBDRM_INCLUDE_DIRS LIBDRM_LIBRARIES)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBDRM REQUIRED_VARS LIBDRM_LIBRARIES LIBDRM_INCLUDE_DIRS)
|
||||
find_package_handle_standard_args(LIBDRM REQUIRED_VARS LIBDRM_LIBRARIES LIBDRM_INCLUDE_DIRS)
|
||||
|
||||
@@ -22,57 +22,59 @@
|
||||
|
||||
IF (NOT WIN32)
|
||||
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the find_path() and find_library() calls
|
||||
find_package(PkgConfig)
|
||||
PKG_CHECK_MODULES(PKG_WAYLAND QUIET wayland-client wayland-server wayland-egl wayland-cursor)
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the find_path() and find_library() calls
|
||||
find_package(PkgConfig)
|
||||
PKG_CHECK_MODULES(PKG_WAYLAND QUIET wayland-client wayland-server wayland-egl wayland-cursor)
|
||||
|
||||
set(WAYLAND_DEFINITIONS ${PKG_WAYLAND_CFLAGS})
|
||||
set(WAYLAND_DEFINITIONS ${PKG_WAYLAND_CFLAGS})
|
||||
|
||||
find_path(WAYLAND_CLIENT_INCLUDE_DIRS NAMES wayland-client.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_CLIENT_LIBRARIES NAMES wayland-client HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_CLIENT_INCLUDE_DIRS AND WAYLAND_CLIENT_LIBRARIES)
|
||||
set(Wayland_Client_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_Client_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES)
|
||||
find_path(WAYLAND_CLIENT_INCLUDE_DIRS NAMES wayland-client.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_CLIENT_LIBRARIES NAMES wayland-client HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_CLIENT_INCLUDE_DIRS AND WAYLAND_CLIENT_LIBRARIES)
|
||||
set(Wayland_Client_FOUND TRUE) # cmake-lint: disable=C0103
|
||||
else()
|
||||
set(Wayland_Client_FOUND FALSE) # cmake-lint: disable=C0103
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_CLIENT_INCLUDE_DIRS WAYLAND_CLIENT_LIBRARIES)
|
||||
|
||||
find_path(WAYLAND_CURSOR_INCLUDE_DIRS NAMES wayland-cursor.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_CURSOR_LIBRARIES NAMES wayland-cursor HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_CURSOR_INCLUDE_DIRS AND WAYLAND_CURSOR_LIBRARIES)
|
||||
set(Wayland_Cursor_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_Cursor_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES)
|
||||
find_path(WAYLAND_CURSOR_INCLUDE_DIRS NAMES wayland-cursor.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_CURSOR_LIBRARIES NAMES wayland-cursor HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_CURSOR_INCLUDE_DIRS AND WAYLAND_CURSOR_LIBRARIES)
|
||||
set(Wayland_Cursor_FOUND TRUE) # cmake-lint: disable=C0103
|
||||
else()
|
||||
set(Wayland_Cursor_FOUND FALSE) # cmake-lint: disable=C0103
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_CURSOR_INCLUDE_DIRS WAYLAND_CURSOR_LIBRARIES)
|
||||
|
||||
find_path(WAYLAND_EGL_INCLUDE_DIRS NAMES wayland-egl.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_EGL_LIBRARIES NAMES wayland-egl HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_EGL_INCLUDE_DIRS AND WAYLAND_EGL_LIBRARIES)
|
||||
set(Wayland_EGL_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_EGL_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES)
|
||||
find_path(WAYLAND_EGL_INCLUDE_DIRS NAMES wayland-egl.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_EGL_LIBRARIES NAMES wayland-egl HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_EGL_INCLUDE_DIRS AND WAYLAND_EGL_LIBRARIES)
|
||||
set(Wayland_EGL_FOUND TRUE) # cmake-lint: disable=C0103
|
||||
else()
|
||||
set(Wayland_EGL_FOUND FALSE) # cmake-lint: disable=C0103
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_EGL_INCLUDE_DIRS WAYLAND_EGL_LIBRARIES)
|
||||
|
||||
find_path(WAYLAND_SERVER_INCLUDE_DIRS NAMES wayland-server.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_SERVER_LIBRARIES NAMES wayland-server HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_SERVER_INCLUDE_DIRS AND WAYLAND_SERVER_LIBRARIES)
|
||||
set(Wayland_Server_FOUND TRUE)
|
||||
else()
|
||||
set(Wayland_Server_FOUND FALSE)
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES)
|
||||
find_path(WAYLAND_SERVER_INCLUDE_DIRS NAMES wayland-server.h HINTS ${PKG_WAYLAND_INCLUDE_DIRS})
|
||||
find_library(WAYLAND_SERVER_LIBRARIES NAMES wayland-server HINTS ${PKG_WAYLAND_LIBRARY_DIRS})
|
||||
if(WAYLAND_SERVER_INCLUDE_DIRS AND WAYLAND_SERVER_LIBRARIES)
|
||||
set(Wayland_Server_FOUND TRUE) # cmake-lint: disable=C0103
|
||||
else()
|
||||
set(Wayland_Server_FOUND FALSE) # cmake-lint: disable=C0103
|
||||
endif()
|
||||
mark_as_advanced(WAYLAND_SERVER_INCLUDE_DIRS WAYLAND_SERVER_LIBRARIES)
|
||||
|
||||
set(WAYLAND_INCLUDE_DIRS ${WAYLAND_CLIENT_INCLUDE_DIRS} ${WAYLAND_SERVER_INCLUDE_DIRS} ${WAYLAND_EGL_INCLUDE_DIRS} ${WAYLAND_CURSOR_INCLUDE_DIRS})
|
||||
set(WAYLAND_LIBRARIES ${WAYLAND_CLIENT_LIBRARIES} ${WAYLAND_SERVER_LIBRARIES} ${WAYLAND_EGL_LIBRARIES} ${WAYLAND_CURSOR_LIBRARIES})
|
||||
mark_as_advanced(WAYLAND_INCLUDE_DIRS WAYLAND_LIBRARIES)
|
||||
set(WAYLAND_INCLUDE_DIRS ${WAYLAND_CLIENT_INCLUDE_DIRS} ${WAYLAND_SERVER_INCLUDE_DIRS}
|
||||
${WAYLAND_EGL_INCLUDE_DIRS} ${WAYLAND_CURSOR_INCLUDE_DIRS})
|
||||
set(WAYLAND_LIBRARIES ${WAYLAND_CLIENT_LIBRARIES} ${WAYLAND_SERVER_LIBRARIES}
|
||||
${WAYLAND_EGL_LIBRARIES} ${WAYLAND_CURSOR_LIBRARIES})
|
||||
mark_as_advanced(WAYLAND_INCLUDE_DIRS WAYLAND_LIBRARIES)
|
||||
|
||||
list(REMOVE_DUPLICATES WAYLAND_INCLUDE_DIRS)
|
||||
list(REMOVE_DUPLICATES WAYLAND_INCLUDE_DIRS)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
find_package_handle_standard_args(Wayland REQUIRED_VARS WAYLAND_LIBRARIES WAYLAND_INCLUDE_DIRS HANDLE_COMPONENTS)
|
||||
find_package_handle_standard_args(Wayland REQUIRED_VARS WAYLAND_LIBRARIES WAYLAND_INCLUDE_DIRS HANDLE_COMPONENTS)
|
||||
|
||||
ENDIF ()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
furo==2022.12.7
|
||||
m2r2==0.3.3
|
||||
Sphinx==5.3.0
|
||||
Sphinx==6.1.1
|
||||
sphinx-copybutton==0.5.1
|
||||
|
||||
@@ -2,6 +2,18 @@ Advanced Usage
|
||||
==============
|
||||
Sunshine will work with the default settings for most users. In some cases you may want to configure Sunshine further.
|
||||
|
||||
Performance Tips
|
||||
----------------
|
||||
|
||||
AMD
|
||||
^^^
|
||||
In Windows, enabling `Enahanced Sync` in AMD's settings may help reduce the latency by an additional frame. This
|
||||
applies to `amfenc` and `libx264`.
|
||||
|
||||
Nvidia
|
||||
^^^^^^
|
||||
Enabling `Fast Sync` in Nvidia settings may help reduce latency.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
The default location for the configuration file is listed below. You can use another location if you
|
||||
@@ -21,7 +33,7 @@ location by modifying the configuration file.
|
||||
Docker /config/
|
||||
Linux ~/.config/sunshine/
|
||||
macOS ~/.config/sunshine/
|
||||
Windows ./config/
|
||||
Windows %ProgramFiles%\\Sunshine\\config
|
||||
========= ===========
|
||||
|
||||
**Example**
|
||||
@@ -81,6 +93,20 @@ min_log_level
|
||||
|
||||
min_log_level = info
|
||||
|
||||
log_path
|
||||
^^^^^^^^
|
||||
|
||||
**Description**
|
||||
The path where the sunshine log is stored.
|
||||
|
||||
**Default**
|
||||
``sunshine.log``
|
||||
|
||||
**Example**
|
||||
.. code-block:: text
|
||||
|
||||
log_path = sunshine.log
|
||||
|
||||
Controls
|
||||
--------
|
||||
|
||||
@@ -341,6 +367,9 @@ dwmflush
|
||||
If enabled, this feature will automatically deactivate if the client framerate exceeds
|
||||
the host monitor's current refresh rate.
|
||||
|
||||
.. Note:: If you disable this option, you may see video stuttering during mouse movement in certain scenarios.
|
||||
It is recommended to leave enabled when possible.
|
||||
|
||||
**Default**
|
||||
``enabled``
|
||||
|
||||
@@ -638,7 +667,7 @@ qp
|
||||
^^
|
||||
|
||||
**Description**
|
||||
Quantitization Parameter. Some devices don't support Constant Bit Rate. For those devices, QP is used instead.
|
||||
Quantization Parameter. Some devices don't support Constant Bit Rate. For those devices, QP is used instead.
|
||||
|
||||
.. Warning:: Higher value means more compression, but less quality.
|
||||
|
||||
@@ -654,7 +683,7 @@ min_threads
|
||||
^^^^^^^^^^^
|
||||
|
||||
**Description**
|
||||
Minimum number of threads used by ffmpeg to encode the video.
|
||||
Minimum number of threads used for software encoding.
|
||||
|
||||
.. Note:: Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually worth it to gain
|
||||
the use of more CPU cores for encoding. The ideal value is the lowest value that can reliably encode at your
|
||||
@@ -824,27 +853,52 @@ nv_preset
|
||||
========== ===========
|
||||
Value Description
|
||||
========== ===========
|
||||
default let ffmpeg decide
|
||||
hp high performance
|
||||
hq high quality
|
||||
slow high quality, 2 passes
|
||||
medium high quality, 1 pass
|
||||
fast high performance, 1 pass
|
||||
bd
|
||||
ll low latency
|
||||
llhq low latency, high quality
|
||||
llhp low latency, high performance
|
||||
lossless lossless
|
||||
losslesshp lossless, high performance
|
||||
p1 fastest (lowest quality)
|
||||
p2 faster (lower quality)
|
||||
p3 fast (low quality)
|
||||
p4 medium (default)
|
||||
p5 slow (good quality)
|
||||
p6 slower (better quality)
|
||||
p7 slowest (best quality)
|
||||
========== ===========
|
||||
|
||||
**Default**
|
||||
``llhq``
|
||||
``p4``
|
||||
|
||||
**Example**
|
||||
.. code-block:: text
|
||||
|
||||
nv_preset = llhq
|
||||
nv_preset = p4
|
||||
|
||||
nv_tune
|
||||
^^^^^^^
|
||||
|
||||
**Description**
|
||||
The encoder tuning profile.
|
||||
|
||||
.. Note:: This option only applies when using nvenc `encoder`_.
|
||||
|
||||
**Choices**
|
||||
|
||||
.. table::
|
||||
:widths: auto
|
||||
|
||||
========== ===========
|
||||
Value Description
|
||||
========== ===========
|
||||
hq high quality
|
||||
ll low latency
|
||||
ull ultra low latency
|
||||
lossless lossless
|
||||
========== ===========
|
||||
|
||||
**Default**
|
||||
``ull``
|
||||
|
||||
**Example**
|
||||
.. code-block:: text
|
||||
|
||||
nv_tune = ull
|
||||
|
||||
nv_rc
|
||||
^^^^^
|
||||
@@ -854,8 +908,6 @@ nv_rc
|
||||
|
||||
.. Note:: This option only applies when using nvenc `encoder`_.
|
||||
|
||||
.. Note:: Moonlight does not currently support variable bitrate, although it can still be selected here.
|
||||
|
||||
**Choices**
|
||||
|
||||
.. table::
|
||||
@@ -864,22 +916,18 @@ nv_rc
|
||||
========== ===========
|
||||
Value Description
|
||||
========== ===========
|
||||
auto let ffmpeg decide
|
||||
constqp constant QP mode
|
||||
cbr constant bitrate
|
||||
cbr_hq constant bitrate, high quality
|
||||
cbr_ld_hq constant bitrate, low delay, high quality
|
||||
vbr variable bitrate
|
||||
vbr_hq variable bitrate, high quality
|
||||
cbr constant bitrate
|
||||
========== ===========
|
||||
|
||||
**Default**
|
||||
``auto``
|
||||
``cbr``
|
||||
|
||||
**Example**
|
||||
.. code-block:: text
|
||||
|
||||
nv_rc = auto
|
||||
nv_rc = cbr
|
||||
|
||||
nv_coder
|
||||
^^^^^^^^
|
||||
@@ -887,7 +935,7 @@ nv_coder
|
||||
**Description**
|
||||
The entropy encoding to use.
|
||||
|
||||
.. Note:: This option only applies when using nvenc `encoder`_.
|
||||
.. Note:: This option only applies when using H264 with nvenc `encoder`_.
|
||||
|
||||
**Choices**
|
||||
|
||||
@@ -898,8 +946,8 @@ nv_coder
|
||||
Value Description
|
||||
========== ===========
|
||||
auto let ffmpeg decide
|
||||
cabac
|
||||
cavlc
|
||||
cabac context adaptive binary arithmetic coding - higher quality
|
||||
cavlc context adaptive variable-length coding - faster decode
|
||||
========== ===========
|
||||
|
||||
**Default**
|
||||
@@ -926,9 +974,9 @@ amd_quality
|
||||
========== ===========
|
||||
Value Description
|
||||
========== ===========
|
||||
default let ffmpeg decide
|
||||
speed fast
|
||||
balanced balance performance and speed
|
||||
speed prefer speed
|
||||
balanced balanced
|
||||
quality prefer quality
|
||||
========== ===========
|
||||
|
||||
**Default**
|
||||
@@ -947,8 +995,6 @@ amd_rc
|
||||
|
||||
.. Note:: This option only applies when using amdvce `encoder`_.
|
||||
|
||||
.. Note:: Moonlight does not currently support variable bitrate, although it can still be selected here.
|
||||
|
||||
**Choices**
|
||||
|
||||
.. table::
|
||||
@@ -957,20 +1003,19 @@ amd_rc
|
||||
=========== ===========
|
||||
Value Description
|
||||
=========== ===========
|
||||
auto let ffmpeg decide
|
||||
constqp constant QP mode
|
||||
cqp constant qp mode
|
||||
cbr constant bitrate
|
||||
vbr_latency variable bitrate, latency constrained
|
||||
vbr_peak variable bitrate, peak constrained
|
||||
=========== ===========
|
||||
|
||||
**Default**
|
||||
``auto``
|
||||
``vbr_latency``
|
||||
|
||||
**Example**
|
||||
.. code-block:: text
|
||||
|
||||
amd_rc = auto
|
||||
amd_rc = vbr_latency
|
||||
|
||||
amd_coder
|
||||
^^^^^^^^^
|
||||
@@ -978,7 +1023,7 @@ amd_coder
|
||||
**Description**
|
||||
The entropy encoding to use.
|
||||
|
||||
.. Note:: This option only applies when using nvenc `encoder`_.
|
||||
.. Note:: This option only applies when using H264 with amdvce `encoder`_.
|
||||
|
||||
**Choices**
|
||||
|
||||
@@ -989,8 +1034,8 @@ amd_coder
|
||||
Value Description
|
||||
========== ===========
|
||||
auto let ffmpeg decide
|
||||
cabac
|
||||
cavlc
|
||||
cabac context adaptive variable-length coding - higher quality
|
||||
cavlc context adaptive binary arithmetic coding - faster decode
|
||||
========== ===========
|
||||
|
||||
**Default**
|
||||
|
||||
@@ -21,19 +21,10 @@ See :ref:`Docker <about/docker:docker>` for additional information.
|
||||
|
||||
Linux
|
||||
-----
|
||||
First, follow the instructions for your preferred package type below.
|
||||
|
||||
Then start sunshine with the following command, unless a start command is listed in the specified package.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sunshine
|
||||
Follow the instructions for your preferred package type below.
|
||||
|
||||
AppImage
|
||||
^^^^^^^^
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/pkg:appimage?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
|
||||
According to AppImageLint the supported distro matrix of the AppImage is below.
|
||||
|
||||
- [✖] Debian oldstable (buster)
|
||||
@@ -82,9 +73,6 @@ Uninstall:
|
||||
|
||||
Debian Package
|
||||
^^^^^^^^^^^^^^
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/pkg:deb?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
|
||||
#. Download ``sunshine-{ubuntu-version}.deb`` and run the following code.
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -103,9 +91,6 @@ Uninstall:
|
||||
|
||||
Flatpak Package
|
||||
^^^^^^^^^^^^^^^
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/pkg:flatpak?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
|
||||
#. Install `Flatpak <https://flatpak.org/setup/>`_ as required.
|
||||
#. Download ``sunshine_{arch}.flatpak`` and run the following code.
|
||||
|
||||
@@ -145,9 +130,6 @@ Uninstall:
|
||||
|
||||
RPM Package
|
||||
^^^^^^^^^^^
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/pkg:rpm?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
|
||||
#. Add `rpmfusion` repositories by running the following code.
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -170,12 +152,11 @@ Uninstall:
|
||||
|
||||
macOS
|
||||
-----
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/os:macos?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
Sunshine on macOS is experimental. Gamepads do not work. Other features may not work as expected.
|
||||
|
||||
pkg
|
||||
^^^
|
||||
.. Warning:: The `pkg` does not include runtime dependencies and should be considered experimental.
|
||||
.. Warning:: The `pkg` does not include runtime dependencies.
|
||||
|
||||
#. Download the ``sunshine.pkg`` file and install it as normal.
|
||||
|
||||
@@ -218,16 +199,14 @@ Uninstall:
|
||||
|
||||
Windows
|
||||
-------
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/os:windows:10?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
|
||||
.. image:: https://img.shields.io/github/issues/lizardbyte/sunshine/os:windows:11?logo=github&style=for-the-badge
|
||||
:alt: GitHub issues by-label
|
||||
|
||||
Installer
|
||||
^^^^^^^^^
|
||||
#. Download and install ``sunshine-windows.exe``
|
||||
|
||||
.. Attention:: You should carefully select or unselect the options you want to install. Do not blindly install or enable
|
||||
features.
|
||||
|
||||
To uninstall, find Sunshine in the list `here <ms-settings:installed-apps>`_ and select "Uninstall" from the overflow
|
||||
menu. Different versions of Windows may provide slightly different steps for uninstall.
|
||||
|
||||
|
||||
@@ -52,6 +52,3 @@ Legacy GitHub Repo
|
||||
|
||||
.. image:: https://img.shields.io/github/release-date/loki-47-6F-64/sunshine?style=for-the-badge&logo=github
|
||||
:alt: GitHub Release Date
|
||||
|
||||
.. image:: https://img.shields.io/github/downloads/loki-47-6F-64/sunshine/total?style=for-the-badge&logo=github
|
||||
:alt: GitHub Releases
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
Usage
|
||||
=====
|
||||
#. See the `setup`_ section for your specific OS.
|
||||
#. Run ``sunshine <directory of conf file>/sunshine.conf``.
|
||||
#. If you did not install the service, then start sunshine with the following command, unless a start command is listed
|
||||
in the specified package :ref:`installation <about/installation:installation>` instructions.
|
||||
|
||||
.. Note:: You do not need to specify a config file. If no config file is entered the default location will be used.
|
||||
.. Note:: A service is a process that runs in the background. Running multiple instances of Sunshine is not
|
||||
advised.
|
||||
|
||||
.. Attention:: The configuration file specified will be created if it doesn't exist.
|
||||
**Basic usage**
|
||||
.. code-block:: bash
|
||||
|
||||
.. Tip:: If using the Linux AppImage, replace ``sunshine`` with ``./sunshine.AppImage``
|
||||
sunshine
|
||||
|
||||
**Specify config file**
|
||||
.. code-block:: bash
|
||||
|
||||
sunshine <directory of conf file>/sunshine.conf
|
||||
|
||||
.. Note:: You do not need to specify a config file. If no config file is entered the default location will be used.
|
||||
|
||||
.. Attention:: The configuration file specified will be created if it doesn't exist.
|
||||
|
||||
#. Configure Sunshine in the web ui
|
||||
|
||||
The web ui is available on `https://localhost:47990 <https://localhost:47990>`_ by default. You may replace
|
||||
`localhost` with your internal ip address.
|
||||
|
||||
.. Attention:: Ignore any warning given by your browser about "insecure website".
|
||||
.. Attention:: Ignore any warning given by your browser about "insecure website". This is due to the SSL certificate
|
||||
being self signed.
|
||||
|
||||
.. Caution:: If running for the first time, make sure to note the username and password Sunshine showed to you,
|
||||
since you cannot get back later!
|
||||
.. Caution:: If running for the first time, make sure to note the username and password that you created.
|
||||
|
||||
**Add games and applications.**
|
||||
This can be configured in the web ui.
|
||||
@@ -26,8 +38,6 @@ Usage
|
||||
list of applications that are started just before running a stream. This is the directory within the GitHub
|
||||
repo.
|
||||
|
||||
.. Attention:: Application list is not fully supported on macOS
|
||||
|
||||
#. In Moonlight, you may need to add the PC manually.
|
||||
#. When Moonlight request you insert the correct pin on sunshine:
|
||||
|
||||
@@ -76,7 +86,7 @@ Sunshine needs access to `uinput` to create mouse and gamepad events.
|
||||
.. code-block::
|
||||
|
||||
[Unit]
|
||||
Description=Sunshine Gamestream Server for Moonlight
|
||||
Description=Sunshine self-hosted game stream host for Moonlight.
|
||||
|
||||
[Service]
|
||||
ExecStart=<see table>
|
||||
@@ -118,7 +128,7 @@ Sunshine needs access to `uinput` to create mouse and gamepad events.
|
||||
|
||||
sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))
|
||||
|
||||
**Disable**
|
||||
**Disable (for Xorg/X11)**
|
||||
.. code-block:: bash
|
||||
|
||||
sudo setcap -r $(readlink -f $(which sunshine))
|
||||
@@ -149,6 +159,32 @@ Windows
|
||||
^^^^^^^
|
||||
For gamepad support, install `ViGEmBus <https://github.com/ViGEm/ViGEmBus/releases/latest>`_
|
||||
|
||||
Sunshine firewall
|
||||
**Add rule**
|
||||
.. code-block:: batch
|
||||
|
||||
cd /d "C:\Program Files\Sunshine\scripts"
|
||||
add-firewall-rule.bat
|
||||
|
||||
**Remove rule**
|
||||
.. code-block:: batch
|
||||
|
||||
cd /d "C:\Program Files\Sunshine\scripts"
|
||||
remove-firewall-rule.bat
|
||||
|
||||
Sunshine service
|
||||
**Enable**
|
||||
.. code-block:: batch
|
||||
|
||||
cd /d "C:\Program Files\Sunshine\scripts"
|
||||
install-service.bat
|
||||
|
||||
**Disable**
|
||||
.. code-block:: batch
|
||||
|
||||
cd /d "C:\Program Files\Sunshine\scripts"
|
||||
uninstall-service.bat
|
||||
|
||||
Shortcuts
|
||||
---------
|
||||
All shortcuts start with ``CTRL + ALT + SHIFT``, just like Moonlight
|
||||
@@ -159,34 +195,43 @@ All shortcuts start with ``CTRL + ALT + SHIFT``, just like Moonlight
|
||||
Application List
|
||||
----------------
|
||||
- Applications should be configured via the web UI.
|
||||
- A basic understanding of working directories and commands is recommended.
|
||||
- A basic understanding of working directories and commands is required.
|
||||
- You can use Environment variables in place of values
|
||||
- ``$(HOME)`` will be replaced by the value of ``$HOME``
|
||||
- ``$$`` will be replaced by ``$``, e.g. ``$$(HOME)`` will be become ``$(HOME)``
|
||||
- ``env`` - Adds or overwrites Environment variables for the commands/applications run by Sunshine
|
||||
- ``"Variable name":"Variable value"``
|
||||
- ``apps`` - The list of applications
|
||||
- Advanced users may want to edit the application list manually. The format is ``json``.
|
||||
- Example application:
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"name":"An App",
|
||||
"cmd":"command to open app",
|
||||
"prep-cmd":[
|
||||
{
|
||||
"do":"some-command",
|
||||
"undo":"undo-that-command"
|
||||
}
|
||||
],
|
||||
"detached":[
|
||||
"some-command",
|
||||
"another-command"
|
||||
]
|
||||
"cmd": "command to open app",
|
||||
"detached": [
|
||||
"some-command",
|
||||
"another-command"
|
||||
],
|
||||
"image-path": "/full-path/to/png-image",
|
||||
"name": "An App",
|
||||
"output": "/full-path/to/command-log-file",
|
||||
"prep-cmd": [
|
||||
{
|
||||
"do": "some-command",
|
||||
"undo": "undo-that-command"
|
||||
}
|
||||
],
|
||||
"working-dir": "/full-path/to/working-directory"
|
||||
}
|
||||
|
||||
- ``cmd`` - The main application
|
||||
- ``detached`` - A list of commands to be run and forgotten about
|
||||
|
||||
- If not specified, a process is started that sleeps indefinitely
|
||||
|
||||
- ``image-path`` - The full path to the cover art image to use.
|
||||
- ``name`` - The name of the application/game
|
||||
- ``output`` - The file where the output of the command is stored
|
||||
- ``detached`` - A list of commands to be run and forgotten about
|
||||
- ``prep-cmd`` - A list of commands to be run before/after the application
|
||||
|
||||
- If any of the prep-commands fail, starting the application is aborted
|
||||
@@ -196,12 +241,9 @@ Application List
|
||||
|
||||
- ``undo`` - Run after the application has terminated
|
||||
|
||||
- This should not fail considering it is supposed to undo the ``do`` commands
|
||||
- If it fails, Sunshine is terminated
|
||||
- Failures of ``undo`` commands are ignored
|
||||
|
||||
- ``cmd`` - The main application
|
||||
|
||||
- If not specified, a process is started that sleeps indefinitely
|
||||
- ``working-dir`` - The working directory to use. If not specified, Sunshine will use the application directory.
|
||||
|
||||
Considerations
|
||||
--------------
|
||||
@@ -214,3 +256,11 @@ Considerations
|
||||
- In addition to the apps listed, one app "Desktop" is hardcoded into Sunshine. It does not start an application,
|
||||
instead it simply starts a stream.
|
||||
- For the Linux flatpak you must prepend commands with ``flatpak-spawn --host``.
|
||||
|
||||
Tutorials
|
||||
---------
|
||||
Tutorial videos are available `here <https://www.youtube.com/playlist?list=PLMYr5_xSeuXAbhxYHz86hA1eCDugoxXY0>`_.
|
||||
|
||||
.. admonition:: Community!
|
||||
|
||||
Tutorials are community generated. Want to contribute? Reach out to us on our discord server.
|
||||
|
||||
@@ -18,6 +18,7 @@ Install Requirements
|
||||
libboost-filesystem-dev \
|
||||
libboost-log-dev \
|
||||
libboost-thread-dev \
|
||||
libboost-program-options-dev \
|
||||
libcap-dev \ # KMS
|
||||
libdrm-dev \ # KMS
|
||||
libevdev-dev \
|
||||
@@ -40,7 +41,7 @@ Install Requirements
|
||||
nvidia-cuda-dev \ # Cuda, NvFBC
|
||||
nvidia-cuda-toolkit # Cuda, NvFBC
|
||||
|
||||
Fedora 35
|
||||
Fedora 36
|
||||
^^^^^^^^^
|
||||
End of Life: TBD
|
||||
|
||||
@@ -79,72 +80,6 @@ Install Requirements
|
||||
pulseaudio-libs-devel \
|
||||
rpm-build # if you want to build an RPM binary package
|
||||
|
||||
Ubuntu 18.04
|
||||
^^^^^^^^^^^^
|
||||
End of Life: April 2028
|
||||
|
||||
Install Repositories
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt update && sudo apt install \
|
||||
software-properties-common \
|
||||
&& add-apt-repository ppa:savoury1/boost-defaults-1.71 && \
|
||||
add-apt-repository ppa:ubuntu-toolchain-r/test && \
|
||||
|
||||
Install Requirements
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt install \
|
||||
build-essential \
|
||||
cmake \
|
||||
gcc-10 \
|
||||
g++-10 \
|
||||
libavdevice-dev \
|
||||
libboost-filesystem1.71-dev \
|
||||
libboost-log1.71-dev \
|
||||
libboost-regex1.71-dev \
|
||||
libboost-thread1.71-dev \
|
||||
libcap-dev \ # KMS
|
||||
libdrm-dev \ # KMS
|
||||
libevdev-dev \
|
||||
libnuma-dev \
|
||||
libopus-dev \
|
||||
libpulse-dev \
|
||||
libssl-dev \
|
||||
libva-dev \
|
||||
libvdpau-dev \
|
||||
libwayland-dev \ # Wayland
|
||||
libx11-dev \ # X11
|
||||
libxcb-shm0-dev \ # X11
|
||||
libxcb-xfixes0-dev \ # X11
|
||||
libxcb1-dev \ # X11
|
||||
libxfixes-dev \ # X11
|
||||
libxrandr-dev \ # X11
|
||||
libxtst-dev \ # X11
|
||||
nodejs \
|
||||
npm \
|
||||
wget
|
||||
|
||||
Update gcc alias
|
||||
.. code-block:: bash
|
||||
|
||||
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10
|
||||
|
||||
Install CuDA
|
||||
.. code-block:: bash
|
||||
|
||||
wget https://developer.download.nvidia.com/compute/cuda/11.4.2/local_installers/cuda_11.4.2_470.57.02_linux.run --progress=bar:force:noscroll -q --show-progress -O ./cuda.run && chmod a+x ./cuda.run
|
||||
./cuda.run --silent --toolkit --toolkitpath=/usr --no-opengl-libs --no-man-page --no-drm && rm ./cuda.run
|
||||
|
||||
Install CMake
|
||||
.. code-block:: bash
|
||||
|
||||
wget https://cmake.org/files/v3.22/cmake-3.22.2-linux-x86_64.sh
|
||||
mkdir /opt/cmake
|
||||
sh /cmake-3.22.2-linux-x86_64.sh --prefix=/opt/cmake --skip-license
|
||||
ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake
|
||||
cmake --version
|
||||
|
||||
Ubuntu 20.04
|
||||
^^^^^^^^^^^^
|
||||
End of Life: April 2030
|
||||
@@ -160,6 +95,7 @@ Install Requirements
|
||||
libboost-filesystem-dev \
|
||||
libboost-log-dev \
|
||||
libboost-thread-dev \
|
||||
libboost-program-options-dev \
|
||||
libcap-dev \ # KMS
|
||||
libdrm-dev \ # KMS
|
||||
libevdev-dev \
|
||||
@@ -206,6 +142,7 @@ Install Requirements
|
||||
libboost-filesystem-dev \
|
||||
libboost-log-dev \
|
||||
libboost-thread-dev \
|
||||
libboost-program-options-dev \
|
||||
libcap-dev \ # KMS
|
||||
libdrm-dev \ # KMS
|
||||
libevdev-dev \
|
||||
@@ -231,27 +168,16 @@ npm dependencies
|
||||
Install npm dependencies.
|
||||
.. code-block:: bash
|
||||
|
||||
pushd ./src_assets/common/assets/web
|
||||
npm install
|
||||
popd
|
||||
|
||||
Build
|
||||
-----
|
||||
.. Attention:: Ensure you are in the build directory created during the clone step earlier before continuing.
|
||||
|
||||
Debian based OSes
|
||||
.. code-block:: bash
|
||||
.. code-block:: bash
|
||||
|
||||
cmake -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 ..
|
||||
cmake ..
|
||||
make -j ${nproc}
|
||||
|
||||
Red Hat based OSes
|
||||
.. code-block:: bash
|
||||
|
||||
cmake -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ ..
|
||||
|
||||
Finally
|
||||
.. code-block:: bash
|
||||
|
||||
make -j ${nproc}
|
||||
cpack -G DEB # optionally, create a deb package
|
||||
cpack -G RPM # optionally, create a rpm package
|
||||
cpack -G DEB # optionally, create a deb package
|
||||
cpack -G RPM # optionally, create a rpm package
|
||||
|
||||
@@ -12,7 +12,7 @@ MacPorts
|
||||
Install Requirements
|
||||
.. code-block:: bash
|
||||
|
||||
sudo port install boost cmake libopus npm9
|
||||
sudo port install avahi boost180 cmake curl libopus npm9 pkgconfig
|
||||
|
||||
Homebrew
|
||||
""""""""
|
||||
@@ -29,9 +29,7 @@ npm dependencies
|
||||
Install npm dependencies.
|
||||
.. code-block:: bash
|
||||
|
||||
pushd ./src_assets/common/assets/web
|
||||
npm install
|
||||
popd
|
||||
|
||||
Build
|
||||
-----
|
||||
@@ -45,5 +43,4 @@ Build
|
||||
cpack -G DragNDrop # optionally, create a macOS dmg package
|
||||
|
||||
If cmake fails complaining to find Boost, try to set the path explicitly.
|
||||
``cmake -DBOOST_ROOT=[boost path] ..``, e.g., ``cmake -DBOOST_ROOT=/opt/local/libexec/boost/1.76 ..``
|
||||
|
||||
``cmake -DBOOST_ROOT=[boost path] ..``, e.g., ``cmake -DBOOST_ROOT=/opt/local/libexec/boost/1.80 ..``
|
||||
|
||||
@@ -3,14 +3,20 @@ Windows
|
||||
|
||||
Requirements
|
||||
------------
|
||||
First you need to install `MSYS2 <https://www.msys2.org>`_, then startup "MSYS2 MinGW 64-bit" and install the
|
||||
following packages using:
|
||||
First you need to install `MSYS2 <https://www.msys2.org>`_, then startup "MSYS2 MinGW 64-bit" and execute the following
|
||||
codes.
|
||||
|
||||
.. code-block:: bash
|
||||
Update all packages:
|
||||
.. code-block:: bash
|
||||
|
||||
pacman -S mingw-w64-x86_64-binutils mingw-w64-x86_64-openssl mingw-w64-x86_64-cmake \
|
||||
mingw-w64-x86_64-toolchain mingw-w64-x86_64-opus mingw-w64-x86_64-x265 mingw-w64-x86_64-boost \
|
||||
git mingw-w64-x86_64-make cmake make gcc
|
||||
pacman -Suy
|
||||
|
||||
Install dependencies:
|
||||
.. code-block:: bash
|
||||
|
||||
pacman -S base-devel cmake diffutils gcc git make mingw-w64-x86_64-binutils \
|
||||
mingw-w64-x86_64-boost mingw-w64-x86_64-cmake mingw-w64-x86_64-curl \
|
||||
mingw-w64-x86_64-openssl mingw-w64-x86_64-opus mingw-w64-x86_64-toolchain
|
||||
|
||||
npm dependencies
|
||||
----------------
|
||||
@@ -19,9 +25,7 @@ Install nodejs and npm. Downloads available `here <https://nodejs.org/en/downloa
|
||||
Install npm dependencies.
|
||||
.. code-block:: bash
|
||||
|
||||
pushd ./src_assets/common/assets/web
|
||||
npm install
|
||||
popd
|
||||
|
||||
Build
|
||||
-----
|
||||
@@ -29,10 +33,8 @@ Build
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
cmake -G"Unix Makefiles" ..
|
||||
cmake -G"MinGW Makefiles" .. # alternatively
|
||||
|
||||
mingw32-make
|
||||
cmake -G "MinGW Makefiles" ..
|
||||
mingw32-make -j$(nproc)
|
||||
|
||||
cpack -G NSIS # optionally, create a windows installer
|
||||
cpack -G ZIP # optionally, create a windows standalone package
|
||||
|
||||
@@ -7,11 +7,9 @@ Source code is tested against the `.clang-format` file for linting errors. The w
|
||||
format testing is `.github/workflows/cpp-clang-format-lint.yml`.
|
||||
|
||||
Test clang-format locally.
|
||||
.. Todo:: This documentation needs to be improved.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
clang-format ...
|
||||
find ./ -iname *.cpp -o -iname *.h -iname *.m -iname *.mm | xargs clang-format -i
|
||||
|
||||
Sphinx
|
||||
------
|
||||
|
||||
21
docs/source/gamestream/gamestream.rst
Normal file
21
docs/source/gamestream/gamestream.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
GameStream
|
||||
==========
|
||||
Nvidia announced that their GameStream service for Nvidia Games clients will be discontinued in February 2023.
|
||||
Luckily, Sunshine performance is now on par with Nvidia GameStream. Many users have even reported that Sunshine
|
||||
outperforms GameStream, so rest assured that Sunshine will be equally performant moving forward.
|
||||
|
||||
Migration
|
||||
---------
|
||||
We have developed a simple migration tool to help you migrate your GameStream games and apps to Sunshine automatically.
|
||||
Please check out our `GSMS <https://github.com/LizardByte/GSMS>`_ project if you're interested in an automated
|
||||
migration option. At the time of writing this GSMS offers the ability to migrate your custom games and apps. The
|
||||
working directory, command, and image are all set in Sunshine's ``apps.json`` file. The box-art image is also copied
|
||||
to a specified directory.
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
Sunshine does have some limitations, as compared to Nvidia GameStream.
|
||||
|
||||
- HDR support is limited and currently HDR is converted to SDR.
|
||||
- Automatic game/application list.
|
||||
- Changing game settings automatically, to optimize streaming.
|
||||
21
docs/source/legal/legal.rst
Normal file
21
docs/source/legal/legal.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
Legal
|
||||
=====
|
||||
.. Attention:: This documentation is for informational purposes only and is not intended as legal advice. If you have
|
||||
any legal questions or concerns about using Sunshine, we recommend consulting with a lawyer.
|
||||
|
||||
Sunshine is licensed under the GPL-3.0 license, which allows for free use and modification of the software.
|
||||
The full text of the license can be reviewed `here <https://github.com/LizardByte/Sunshine/blob/master/LICENSE>`_.
|
||||
|
||||
Commercial Use
|
||||
--------------
|
||||
Sunshine can be used in commercial applications without any limitations. This means that businesses and organizations
|
||||
can use Sunshine to create and sell products or services without needing to seek permission or pay a fee.
|
||||
|
||||
However, it is important to note that the GPL-3.0 license does not grant any rights to distribute or sell the encoders
|
||||
contained within Sunshine. If you plan to sell access to Sunshine as part of their distribution, you are responsible
|
||||
for obtaining the necessary licenses to do so. This may include obtaining a license from the
|
||||
Motion Picture Experts Group (MPEG-LA) and/or any other necessary licensing requirements.
|
||||
|
||||
In summary, while Sunshine is free to use, it is the user's responsibility to ensure compliance with all applicable
|
||||
licensing requirements when redistributing the software as part of a commercial offering. If you have any questions or
|
||||
concerns about using Sunshine in a commercial setting, we recommend consulting with a lawyer.
|
||||
@@ -9,6 +9,12 @@
|
||||
about/usage
|
||||
about/advanced_usage
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: GameStream
|
||||
|
||||
gamestream/gamestream
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Troubleshooting
|
||||
@@ -34,3 +40,9 @@
|
||||
contributing/contributing
|
||||
contributing/localization
|
||||
contributing/testing
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Legal
|
||||
|
||||
legal/legal
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
General
|
||||
=======
|
||||
|
||||
Forgotten Credentials
|
||||
---------------------
|
||||
If you forgot your credentials to the web UI, try this.
|
||||
.. code-block:: bash
|
||||
|
||||
sunshine --creds <new username> <new password>
|
||||
|
||||
Web UI Access
|
||||
-------------
|
||||
Can't access the web UI?
|
||||
#. Check firewall rules.
|
||||
|
||||
Nvidia issues
|
||||
-------------
|
||||
NvFBC, NvENC, or general issues with Nvidia graphics card.
|
||||
- Consumer grade Nvidia cards are software limited to a specific number of encodes. See
|
||||
`Video Encode and Decode GPU Support Matrix <https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new>`_
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
Linux
|
||||
=====
|
||||
|
||||
KMS Streaming fails
|
||||
-------------------
|
||||
If screencasting fails with KMS, you may need to run the following to force unprivileged screencasting.
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
macOS
|
||||
=====
|
||||
|
||||
Dynamic session lookup failed
|
||||
-----------------------------
|
||||
If you get this error:
|
||||
`Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that
|
||||
org.freedesktop.dbus-session.plist is loaded!`
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Windows
|
||||
=======
|
||||
No gamepad is detected.
|
||||
#. Verify that you've installed `ViGEmBus <https://github.com/ViGEm/ViGEmBus/releases/latest>`_.
|
||||
|
||||
No gamepad detected
|
||||
-------------------
|
||||
#. Verify that you've installed `ViGEmBus <https://github.com/ViGEm/ViGEmBus/releases/latest>`_.
|
||||
|
||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.2.1",
|
||||
"bootstrap": "5.2.3",
|
||||
"vue": "2.6.12"
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ prepare() {
|
||||
}
|
||||
|
||||
build() {
|
||||
pushd "$pkgname/src_assets/common/assets/web"
|
||||
pushd "$pkgname"
|
||||
npm install
|
||||
popd
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ modules:
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- cd tools/build && bison -y -d -o src/engine/jamgram.cpp src/engine/jamgram.y
|
||||
- ./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=system,thread,log || cat bootstrap.log
|
||||
- ./bootstrap.sh --prefix=$FLATPAK_DEST --with-libraries=system,thread,log,program_options || cat bootstrap.log
|
||||
- ./b2 install variant=release link=static,shared runtime-link=shared cxxflags="$CXXFLAGS" linkflags="$LDFLAGS"
|
||||
-j $FLATPAK_BUILDER_N_JOBS
|
||||
sources:
|
||||
@@ -142,7 +142,7 @@ modules:
|
||||
NPM_CONFIG_LOGLEVEL: info
|
||||
build-commands:
|
||||
# Install npm dependencies
|
||||
- cd ${FLATPAK_BUILDER_BUILDDIR}/src_assets/common/assets/web && npm install
|
||||
- cd ${FLATPAK_BUILDER_BUILDDIR} && npm install
|
||||
config-opts:
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
- -DCMAKE_INSTALL_PREFIX=/app
|
||||
|
||||
@@ -56,7 +56,7 @@ platform darwin {
|
||||
}
|
||||
|
||||
pre-build {
|
||||
system -W ${worksrcpath}/src_assets/common/assets/web "npm install"
|
||||
system -W ${worksrcpath} "npm install"
|
||||
}
|
||||
|
||||
notes-append "Run @PROJECT_NAME@ by executing 'sunshine <path to user config>', e.g. 'sunshine ~/sunshine.conf' "
|
||||
|
||||
@@ -39,6 +39,15 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
1,
|
||||
1,
|
||||
platf::speaker::map_stereo,
|
||||
96000,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
platf::speaker::map_stereo,
|
||||
512000,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
@@ -46,6 +55,7 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
4,
|
||||
2,
|
||||
platf::speaker::map_surround51,
|
||||
256000,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
@@ -53,6 +63,7 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
6,
|
||||
0,
|
||||
platf::speaker::map_surround51,
|
||||
1536000,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
@@ -60,6 +71,7 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
5,
|
||||
3,
|
||||
platf::speaker::map_surround71,
|
||||
450000,
|
||||
},
|
||||
{
|
||||
SAMPLE_RATE,
|
||||
@@ -67,6 +79,7 @@ opus_stream_config_t stream_configs[MAX_STREAM_CONFIG] {
|
||||
8,
|
||||
0,
|
||||
platf::speaker::map_surround71,
|
||||
2048000,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,9 +87,10 @@ auto control_shared = safe::make_shared<audio_ctx_t>(start_audio_control, stop_a
|
||||
|
||||
void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
||||
auto packets = mail::man->queue<packet_t>(mail::audio_packets);
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
|
||||
// FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
// Encoding takes place on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::high);
|
||||
|
||||
opus_t opus { opus_multistream_encoder_create(
|
||||
stream->sampleRate,
|
||||
@@ -84,17 +98,15 @@ void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
||||
stream->streams,
|
||||
stream->coupledStreams,
|
||||
stream->mapping,
|
||||
OPUS_APPLICATION_AUDIO,
|
||||
OPUS_APPLICATION_RESTRICTED_LOWDELAY,
|
||||
nullptr) };
|
||||
|
||||
// For some reason, audio is crackling when the encoder is set to constant bitstream.
|
||||
// We simulate a constant bitstream with OPUS_SET_BITRATE(OPUS_BITRATE_MAX) -->
|
||||
// which tries to occupy as much space as possible in the packet
|
||||
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(OPUS_BITRATE_MAX));
|
||||
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_BITRATE(stream->bitrate));
|
||||
opus_multistream_encoder_ctl(opus.get(), OPUS_SET_VBR(0));
|
||||
|
||||
auto frame_size = config.packetDuration * stream->sampleRate / 1000;
|
||||
while(auto sample = samples->pop()) {
|
||||
buffer_t packet { 1400 }; // 1KB
|
||||
buffer_t packet { 1400 };
|
||||
|
||||
int bytes = opus_multistream_encode(opus.get(), sample->data(), frame_size, std::begin(packet), packet.size());
|
||||
if(bytes < 0) {
|
||||
@@ -104,14 +116,6 @@ void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Even with OPUS_SET_BITRATE(OPUS_BITRATE_MAX), silent packets are smaller than the rest
|
||||
// Drop silent packets to ensure Moonlight won't complain
|
||||
// A packet size of 128 seems a reasonable enough threshold
|
||||
if(bytes < 128) {
|
||||
BOOST_LOG(verbose) << "Dropped silent packet"sv;
|
||||
continue;
|
||||
}
|
||||
|
||||
packet.fake_resize(bytes);
|
||||
packets->raise(channel_data, std::move(packet));
|
||||
}
|
||||
@@ -119,9 +123,7 @@ void encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
||||
|
||||
void capture(safe::mail_t mail, config_t config, void *channel_data) {
|
||||
auto shutdown_event = mail->event<bool>(mail::shutdown);
|
||||
|
||||
// FIXME: Pick correct opus_stream_config_t based on config.channels
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
auto stream = &stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])];
|
||||
|
||||
auto ref = control_shared.ref();
|
||||
if(!ref) {
|
||||
@@ -174,6 +176,9 @@ void capture(safe::mail_t mail, config_t config, void *channel_data) {
|
||||
}
|
||||
}
|
||||
|
||||
// Capture takes place on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::critical);
|
||||
|
||||
auto samples = std::make_shared<sample_queue_t::element_type>(30);
|
||||
std::thread thread { encodeThread, samples, config, channel_data };
|
||||
|
||||
@@ -225,7 +230,7 @@ int map_stream(int channels, bool quality) {
|
||||
int shift = quality ? 1 : 0;
|
||||
switch(channels) {
|
||||
case 2:
|
||||
return STEREO;
|
||||
return STEREO + shift;
|
||||
case 6:
|
||||
return SURROUND51 + shift;
|
||||
case 8:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace audio {
|
||||
enum stream_config_e : int {
|
||||
STEREO,
|
||||
HIGH_STEREO,
|
||||
SURROUND51,
|
||||
HIGH_SURROUND51,
|
||||
SURROUND71,
|
||||
@@ -19,6 +20,7 @@ struct opus_stream_config_t {
|
||||
int streams;
|
||||
int coupledStreams;
|
||||
const std::uint8_t *mapping;
|
||||
int bitrate;
|
||||
};
|
||||
|
||||
extern opus_stream_config_t stream_configs[MAX_STREAM_CONFIG];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
extern "C" {
|
||||
#include <cbs/cbs_h264.h>
|
||||
#include <cbs/cbs_h265.h>
|
||||
#include <cbs/video_levels.h>
|
||||
#include <cbs/h264_levels.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
}
|
||||
|
||||
203
src/config.cpp
203
src/config.cpp
@@ -25,50 +25,71 @@ using namespace std::literals;
|
||||
namespace config {
|
||||
|
||||
namespace nv {
|
||||
#ifdef __APPLE__
|
||||
// values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build
|
||||
#define NV_ENC_TUNING_INFO_HIGH_QUALITY 1
|
||||
#define NV_ENC_TUNING_INFO_LOW_LATENCY 2
|
||||
#define NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY 3
|
||||
#define NV_ENC_TUNING_INFO_LOSSLESS 4
|
||||
#define NV_ENC_PARAMS_RC_CONSTQP 0x0
|
||||
#define NV_ENC_PARAMS_RC_VBR 0x1
|
||||
#define NV_ENC_PARAMS_RC_CBR 0x2
|
||||
#define NV_ENC_H264_ENTROPY_CODING_MODE_CABAC 1
|
||||
#define NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC 2
|
||||
#else
|
||||
#include <ffnvcodec/nvEncodeAPI.h>
|
||||
#endif
|
||||
|
||||
enum preset_e : int {
|
||||
_default = 0,
|
||||
slow,
|
||||
medium,
|
||||
fast,
|
||||
hp,
|
||||
hq,
|
||||
bd,
|
||||
ll_default,
|
||||
llhq,
|
||||
llhp,
|
||||
lossless_default, // lossless presets must be the last ones
|
||||
lossless_hp,
|
||||
p1 = 12, // PRESET_P1, // must be kept in sync with <libavcodec/nvenc.h>
|
||||
p2, // PRESET_P2,
|
||||
p3, // PRESET_P3,
|
||||
p4, // PRESET_P4,
|
||||
p5, // PRESET_P5,
|
||||
p6, // PRESET_P6,
|
||||
p7 // PRESET_P7
|
||||
};
|
||||
|
||||
enum tune_e : int {
|
||||
hq = NV_ENC_TUNING_INFO_HIGH_QUALITY,
|
||||
ll = NV_ENC_TUNING_INFO_LOW_LATENCY,
|
||||
ull = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
|
||||
lossless = NV_ENC_TUNING_INFO_LOSSLESS
|
||||
};
|
||||
|
||||
enum rc_e : int {
|
||||
constqp = 0x0, /**< Constant QP mode */
|
||||
vbr = 0x1, /**< Variable bitrate mode */
|
||||
cbr = 0x2, /**< Constant bitrate mode */
|
||||
cbr_ld_hq = 0x8, /**< low-delay CBR, high quality */
|
||||
cbr_hq = 0x10, /**< CBR, high quality (slower) */
|
||||
vbr_hq = 0x20 /**< VBR, high quality (slower) */
|
||||
constqp = NV_ENC_PARAMS_RC_CONSTQP, /**< Constant QP mode */
|
||||
vbr = NV_ENC_PARAMS_RC_VBR, /**< Variable bitrate mode */
|
||||
cbr = NV_ENC_PARAMS_RC_CBR /**< Constant bitrate mode */
|
||||
};
|
||||
|
||||
enum coder_e : int {
|
||||
_auto = 0,
|
||||
cabac,
|
||||
cavlc
|
||||
cabac = NV_ENC_H264_ENTROPY_CODING_MODE_CABAC,
|
||||
cavlc = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC,
|
||||
};
|
||||
|
||||
std::optional<preset_e> preset_from_view(const std::string_view &preset) {
|
||||
#define _CONVERT_(x) \
|
||||
if(preset == #x##sv) return x
|
||||
_CONVERT_(slow);
|
||||
_CONVERT_(medium);
|
||||
_CONVERT_(fast);
|
||||
_CONVERT_(hp);
|
||||
_CONVERT_(bd);
|
||||
_CONVERT_(ll_default);
|
||||
_CONVERT_(llhq);
|
||||
_CONVERT_(llhp);
|
||||
_CONVERT_(lossless_default);
|
||||
_CONVERT_(lossless_hp);
|
||||
if(preset == "default"sv) return _default;
|
||||
_CONVERT_(p1);
|
||||
_CONVERT_(p2);
|
||||
_CONVERT_(p3);
|
||||
_CONVERT_(p4);
|
||||
_CONVERT_(p5);
|
||||
_CONVERT_(p6);
|
||||
_CONVERT_(p7);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<tune_e> tune_from_view(const std::string_view &tune) {
|
||||
#define _CONVERT_(x) \
|
||||
if(tune == #x##sv) return x
|
||||
_CONVERT_(hq);
|
||||
_CONVERT_(ll);
|
||||
_CONVERT_(ull);
|
||||
_CONVERT_(lossless);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -79,9 +100,6 @@ std::optional<rc_e> rc_from_view(const std::string_view &rc) {
|
||||
_CONVERT_(constqp);
|
||||
_CONVERT_(vbr);
|
||||
_CONVERT_(cbr);
|
||||
_CONVERT_(cbr_hq);
|
||||
_CONVERT_(vbr_hq);
|
||||
_CONVERT_(cbr_ld_hq);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -96,57 +114,76 @@ int coder_from_view(const std::string_view &coder) {
|
||||
} // namespace nv
|
||||
|
||||
namespace amd {
|
||||
enum quality_e : int {
|
||||
_default = 0,
|
||||
speed,
|
||||
balanced,
|
||||
#ifdef __APPLE__
|
||||
// values accurate as of 27/12/2022, but aren't strictly necessary for MacOS build
|
||||
#define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED 10
|
||||
#define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY 0
|
||||
#define AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED 5
|
||||
#define AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED 1
|
||||
#define AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY 2
|
||||
#define AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED 0
|
||||
#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP 0
|
||||
#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR 3
|
||||
#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2
|
||||
#define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 1
|
||||
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP 0
|
||||
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1
|
||||
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2
|
||||
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3
|
||||
#define AMF_VIDEO_ENCODER_UNDEFINED 0
|
||||
#define AMF_VIDEO_ENCODER_CABAC 1
|
||||
#define AMF_VIDEO_ENCODER_CALV 2
|
||||
#else
|
||||
#include <AMF/components/VideoEncoderHEVC.h>
|
||||
#include <AMF/components/VideoEncoderVCE.h>
|
||||
#endif
|
||||
|
||||
enum class quality_hevc_e : int {
|
||||
speed = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_SPEED,
|
||||
quality = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_QUALITY,
|
||||
balanced = AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET_BALANCED
|
||||
};
|
||||
|
||||
enum class quality_h264_e : int {
|
||||
speed = AMF_VIDEO_ENCODER_QUALITY_PRESET_SPEED,
|
||||
quality = AMF_VIDEO_ENCODER_QUALITY_PRESET_QUALITY,
|
||||
balanced = AMF_VIDEO_ENCODER_QUALITY_PRESET_BALANCED
|
||||
};
|
||||
|
||||
enum class rc_hevc_e : int {
|
||||
constqp, /**< Constant QP mode */
|
||||
vbr_latency, /**< Latency Constrained Variable Bitrate */
|
||||
vbr_peak, /**< Peak Constrained Variable Bitrate */
|
||||
cbr, /**< Constant bitrate mode */
|
||||
cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP,
|
||||
vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR,
|
||||
vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR,
|
||||
cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR
|
||||
};
|
||||
|
||||
enum class rc_h264_e : int {
|
||||
constqp, /**< Constant QP mode */
|
||||
cbr, /**< Constant bitrate mode */
|
||||
vbr_peak, /**< Peak Constrained Variable Bitrate */
|
||||
vbr_latency, /**< Latency Constrained Variable Bitrate */
|
||||
cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP,
|
||||
vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR,
|
||||
vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR,
|
||||
cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR
|
||||
};
|
||||
|
||||
enum coder_e : int {
|
||||
_auto = 0,
|
||||
cabac,
|
||||
cavlc
|
||||
_auto = AMF_VIDEO_ENCODER_UNDEFINED,
|
||||
cabac = AMF_VIDEO_ENCODER_CABAC,
|
||||
cavlc = AMF_VIDEO_ENCODER_CALV
|
||||
};
|
||||
|
||||
std::optional<quality_e> quality_from_view(const std::string_view &quality) {
|
||||
std::optional<int> quality_from_view(const std::string_view &quality_type, int codec) {
|
||||
#define _CONVERT_(x) \
|
||||
if(quality == #x##sv) return x
|
||||
if(quality_type == #x##sv) return codec == 0 ? (int)quality_hevc_e::x : (int)quality_h264_e::x
|
||||
_CONVERT_(quality);
|
||||
_CONVERT_(speed);
|
||||
_CONVERT_(balanced);
|
||||
if(quality == "default"sv) return _default;
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> rc_h264_from_view(const std::string_view &rc) {
|
||||
std::optional<int> rc_from_view(const std::string_view &rc, int codec) {
|
||||
#define _CONVERT_(x) \
|
||||
if(rc == #x##sv) return (int)rc_h264_e::x
|
||||
_CONVERT_(constqp);
|
||||
_CONVERT_(vbr_latency);
|
||||
_CONVERT_(vbr_peak);
|
||||
_CONVERT_(cbr);
|
||||
#undef _CONVERT_
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> rc_hevc_from_view(const std::string_view &rc) {
|
||||
#define _CONVERT_(x) \
|
||||
if(rc == #x##sv) return (int)rc_hevc_e::x
|
||||
_CONVERT_(constqp);
|
||||
if(rc == #x##sv) return codec == 0 ? (int)rc_hevc_e::x : (int)rc_h264_e::x
|
||||
_CONVERT_(cqp);
|
||||
_CONVERT_(vbr_latency);
|
||||
_CONVERT_(vbr_peak);
|
||||
_CONVERT_(cbr);
|
||||
@@ -211,16 +248,19 @@ video_t video {
|
||||
}, // software
|
||||
|
||||
{
|
||||
nv::llhq,
|
||||
std::nullopt,
|
||||
-1 }, // nv
|
||||
nv::p4, // preset
|
||||
nv::ull, // tune
|
||||
nv::cbr, // rc
|
||||
nv::_auto // coder
|
||||
}, // nv
|
||||
|
||||
{
|
||||
amd::balanced,
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
-1 }, // amd
|
||||
|
||||
(int)amd::quality_h264_e::balanced, // quality (h264)
|
||||
(int)amd::quality_hevc_e::balanced, // quality (hevc)
|
||||
(int)amd::rc_h264_e::vbr_latency, // rate control (h264)
|
||||
(int)amd::rc_hevc_e::vbr_latency, // rate control (hevc)
|
||||
(int)amd::coder_e::_auto, // coder
|
||||
}, // amd
|
||||
{
|
||||
0,
|
||||
0,
|
||||
@@ -296,6 +336,7 @@ sunshine_t sunshine {
|
||||
platf::appdata().string() + "/sunshine.conf", // config file
|
||||
{}, // cmd args
|
||||
47989,
|
||||
platf::appdata().string() + "/sunshine.log", // log file
|
||||
};
|
||||
|
||||
bool endline(char ch) {
|
||||
@@ -716,17 +757,23 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
string_f(vars, "sw_preset", video.sw.preset);
|
||||
string_f(vars, "sw_tune", video.sw.tune);
|
||||
int_f(vars, "nv_preset", video.nv.preset, nv::preset_from_view);
|
||||
int_f(vars, "nv_tune", video.nv.tune, nv::tune_from_view);
|
||||
int_f(vars, "nv_rc", video.nv.rc, nv::rc_from_view);
|
||||
int_f(vars, "nv_coder", video.nv.coder, nv::coder_from_view);
|
||||
|
||||
int_f(vars, "amd_quality", video.amd.quality, amd::quality_from_view);
|
||||
std::string quality;
|
||||
string_f(vars, "amd_quality", quality);
|
||||
if(!quality.empty()) {
|
||||
video.amd.quality_h264 = amd::quality_from_view(quality, 1);
|
||||
video.amd.quality_hevc = amd::quality_from_view(quality, 0);
|
||||
}
|
||||
|
||||
std::string rc;
|
||||
string_f(vars, "amd_rc", rc);
|
||||
int_f(vars, "amd_coder", video.amd.coder, amd::coder_from_view);
|
||||
if(!rc.empty()) {
|
||||
video.amd.rc_h264 = amd::rc_h264_from_view(rc);
|
||||
video.amd.rc_hevc = amd::rc_hevc_from_view(rc);
|
||||
video.amd.rc_h264 = amd::rc_from_view(rc, 1);
|
||||
video.amd.rc_hevc = amd::rc_from_view(rc, 0);
|
||||
}
|
||||
|
||||
int_f(vars, "vt_coder", video.vt.coder, vt::coder_from_view);
|
||||
@@ -742,7 +789,7 @@ void apply_config(std::unordered_map<std::string, std::string> &&vars) {
|
||||
path_f(vars, "pkey", nvhttp.pkey);
|
||||
path_f(vars, "cert", nvhttp.cert);
|
||||
string_f(vars, "sunshine_name", nvhttp.sunshine_name);
|
||||
|
||||
path_f(vars, "log_path", config::sunshine.log_file);
|
||||
path_f(vars, "file_state", nvhttp.file_state);
|
||||
|
||||
// Must be run after "file_state"
|
||||
|
||||
@@ -23,12 +23,14 @@ struct video_t {
|
||||
|
||||
struct {
|
||||
std::optional<int> preset;
|
||||
std::optional<int> tune;
|
||||
std::optional<int> rc;
|
||||
int coder;
|
||||
} nv;
|
||||
|
||||
struct {
|
||||
std::optional<int> quality;
|
||||
std::optional<int> quality_h264;
|
||||
std::optional<int> quality_hevc;
|
||||
std::optional<int> rc_h264;
|
||||
std::optional<int> rc_hevc;
|
||||
int coder;
|
||||
@@ -120,6 +122,7 @@ struct sunshine_t {
|
||||
} cmd;
|
||||
|
||||
std::uint16_t port;
|
||||
std::string log_file;
|
||||
};
|
||||
|
||||
extern video_t video;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "process.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
@@ -68,7 +69,7 @@ void print_req(const req_https_t &request) {
|
||||
}
|
||||
|
||||
void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto address = request->remote_endpoint().address().to_string();
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
|
||||
@@ -77,7 +78,7 @@ void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
}
|
||||
|
||||
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto address = request->remote_endpoint().address().to_string();
|
||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
||||
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||
{ "Location", path }
|
||||
@@ -86,7 +87,7 @@ void send_redirect(resp_https_t response, req_https_t request, const char *path)
|
||||
}
|
||||
|
||||
bool authenticate(resp_https_t response, req_https_t request) {
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto address = request->remote_endpoint().address().to_string();
|
||||
auto ip_type = net::from_address(address);
|
||||
|
||||
if(ip_type > http::origin_web_ui_allowed) {
|
||||
@@ -274,6 +275,17 @@ void getApps(resp_https_t response, req_https_t request) {
|
||||
response->write(content);
|
||||
}
|
||||
|
||||
void getLogs(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::string content = read_file(config::sunshine.log_file.c_str());
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "text/plain");
|
||||
response->write(SimpleWeb::StatusCode::success_ok, content, headers);
|
||||
}
|
||||
|
||||
void saveApp(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
@@ -468,6 +480,7 @@ void getConfig(resp_https_t response, req_https_t request) {
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||
outputTree.put("restart_supported", platf::restart_supported());
|
||||
|
||||
auto vars = config::parse_config(read_file(config::sunshine.config_file.c_str()));
|
||||
|
||||
@@ -511,6 +524,37 @@ void saveConfig(resp_https_t response, req_https_t request) {
|
||||
}
|
||||
}
|
||||
|
||||
void restart(resp_https_t response, req_https_t request) {
|
||||
if(!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
if(!platf::restart_supported()) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Restart is not currently supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!platf::restart()) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Restart failed");
|
||||
return;
|
||||
}
|
||||
|
||||
outputTree.put("status", true);
|
||||
}
|
||||
|
||||
void savePassword(resp_https_t response, req_https_t request) {
|
||||
if(!config::sunshine.username.empty() && !authenticate(response, request)) return;
|
||||
|
||||
@@ -636,11 +680,8 @@ void start() {
|
||||
|
||||
auto port_https = map_port(PORT_HTTPS);
|
||||
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
|
||||
https_server_t server { ctx, 0 };
|
||||
server.default_resource = not_found;
|
||||
https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };
|
||||
server.default_resource["GET"] = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/pin$"]["GET"] = getPinPage;
|
||||
server.resource["^/apps$"]["GET"] = getAppsPage;
|
||||
@@ -651,9 +692,11 @@ void start() {
|
||||
server.resource["^/troubleshooting$"]["GET"] = getTroubleshootingPage;
|
||||
server.resource["^/api/pin$"]["POST"] = savePin;
|
||||
server.resource["^/api/apps$"]["GET"] = getApps;
|
||||
server.resource["^/api/logs$"]["GET"] = getLogs;
|
||||
server.resource["^/api/apps$"]["POST"] = saveApp;
|
||||
server.resource["^/api/config$"]["GET"] = getConfig;
|
||||
server.resource["^/api/config$"]["POST"] = saveConfig;
|
||||
server.resource["^/api/restart$"]["POST"] = restart;
|
||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
||||
server.resource["^/api/clients/unpair$"]["POST"] = unpairAll;
|
||||
@@ -666,19 +709,11 @@ void start() {
|
||||
server.config.address = "0.0.0.0"s;
|
||||
server.config.port = port_https;
|
||||
|
||||
try {
|
||||
server.bind();
|
||||
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port_https << "]";
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_https << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
auto accept_and_run = [&](auto *server) {
|
||||
try {
|
||||
server->accept_and_run();
|
||||
server->start([](unsigned short port) {
|
||||
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]";
|
||||
});
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling server->stop() from a different thread
|
||||
@@ -686,7 +721,7 @@ void start() {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server to port ["sv << port_https << "]: "sv << err.what();
|
||||
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server on port ["sv << port_https << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
#include <openssl/pem.h>
|
||||
|
||||
namespace crypto {
|
||||
using big_num_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
// using rsa_t = util::safe_ptr<RSA, RSA_free>;
|
||||
using asn1_string_t = util::safe_ptr<ASN1_STRING, ASN1_STRING_free>;
|
||||
|
||||
cert_chain_t::cert_chain_t() : _certs {}, _cert_ctx { X509_STORE_CTX_new() } {}
|
||||
@@ -315,12 +313,7 @@ aes_t gen_aes_key(const std::array<uint8_t, 16> &salt, const std::string_view &p
|
||||
|
||||
sha256_t hash(const std::string_view &plaintext) {
|
||||
sha256_t hsh;
|
||||
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
SHA256_Update(&sha256, plaintext.data(), plaintext.size());
|
||||
SHA256_Final(hsh.data(), &sha256);
|
||||
|
||||
EVP_Digest(plaintext.data(), plaintext.size(), hsh.data(), nullptr, EVP_sha256(), nullptr);
|
||||
return hsh;
|
||||
}
|
||||
|
||||
@@ -409,17 +402,20 @@ std::vector<uint8_t> sign(const pkey_t &pkey, const std::string_view &data, cons
|
||||
|
||||
creds_t gen_creds(const std::string_view &cn, std::uint32_t key_bits) {
|
||||
x509_t x509 { X509_new() };
|
||||
pkey_t pkey { EVP_PKEY_new() };
|
||||
pkey_ctx_t ctx { EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr) };
|
||||
pkey_t pkey;
|
||||
|
||||
big_num_t big_num { BN_new() };
|
||||
BN_set_word(big_num.get(), RSA_F4);
|
||||
|
||||
auto rsa = RSA_new();
|
||||
RSA_generate_key_ex(rsa, key_bits, big_num.get(), nullptr);
|
||||
EVP_PKEY_assign_RSA(pkey.get(), rsa);
|
||||
EVP_PKEY_keygen_init(ctx.get());
|
||||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), key_bits);
|
||||
EVP_PKEY_keygen(ctx.get(), &pkey);
|
||||
|
||||
X509_set_version(x509.get(), 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(x509.get()), 0);
|
||||
|
||||
// Generate a real serial number to avoid SEC_ERROR_REUSED_ISSUER_AND_SERIAL with Firefox
|
||||
bignum_t serial { BN_new() };
|
||||
BN_rand(serial.get(), 159, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY); // 159 bits to fit in 20 bytes in DER format
|
||||
BN_set_negative(serial.get(), 0); // Serial numbers must be positive
|
||||
BN_to_ASN1_INTEGER(serial.get(), X509_get_serialNumber(x509.get()));
|
||||
|
||||
constexpr auto year = 60 * 60 * 24 * 365;
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
|
||||
14
src/crypto.h
14
src/crypto.h
@@ -30,6 +30,8 @@ using cipher_ctx_t = util::safe_ptr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
|
||||
using md_ctx_t = util::safe_ptr<EVP_MD_CTX, md_ctx_destroy>;
|
||||
using bio_t = util::safe_ptr<BIO, BIO_free_all>;
|
||||
using pkey_t = util::safe_ptr<EVP_PKEY, EVP_PKEY_free>;
|
||||
using pkey_ctx_t = util::safe_ptr<EVP_PKEY_CTX, EVP_PKEY_CTX_free>;
|
||||
using bignum_t = util::safe_ptr<BIGNUM, BN_free>;
|
||||
|
||||
sha256_t hash(const std::string_view &plaintext);
|
||||
|
||||
@@ -82,8 +84,8 @@ public:
|
||||
|
||||
class ecb_t : public cipher_t {
|
||||
public:
|
||||
ecb_t() = default;
|
||||
ecb_t(ecb_t &&) noexcept = default;
|
||||
ecb_t() = default;
|
||||
ecb_t(ecb_t &&) noexcept = default;
|
||||
ecb_t &operator=(ecb_t &&) noexcept = default;
|
||||
|
||||
ecb_t(const aes_t &key, bool padding = true);
|
||||
@@ -94,8 +96,8 @@ public:
|
||||
|
||||
class gcm_t : public cipher_t {
|
||||
public:
|
||||
gcm_t() = default;
|
||||
gcm_t(gcm_t &&) noexcept = default;
|
||||
gcm_t() = default;
|
||||
gcm_t(gcm_t &&) noexcept = default;
|
||||
gcm_t &operator=(gcm_t &&) noexcept = default;
|
||||
|
||||
gcm_t(const crypto::aes_t &key, bool padding = true);
|
||||
@@ -113,8 +115,8 @@ public:
|
||||
|
||||
class cbc_t : public cipher_t {
|
||||
public:
|
||||
cbc_t() = default;
|
||||
cbc_t(cbc_t &&) noexcept = default;
|
||||
cbc_t() = default;
|
||||
cbc_t(cbc_t &&) noexcept = default;
|
||||
cbc_t &operator=(cbc_t &&) noexcept = default;
|
||||
|
||||
cbc_t(const crypto::aes_t &key, bool padding = true);
|
||||
|
||||
68
src/main.cpp
68
src/main.cpp
@@ -101,6 +101,7 @@ int entry(const char *name, int argc, char *argv[]) {
|
||||
}
|
||||
} // namespace version
|
||||
|
||||
|
||||
void log_flush() {
|
||||
sink->flush();
|
||||
}
|
||||
@@ -136,23 +137,71 @@ std::map<std::string_view, std::function<int(const char *name, int argc, char **
|
||||
{ "version"sv, version::entry }
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
LRESULT CALLBACK SessionMonitorWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
|
||||
switch(uMsg) {
|
||||
case WM_ENDSESSION: {
|
||||
// Raise a SIGINT to trigger our cleanup logic and terminate ourselves
|
||||
std::cout << "Received WM_ENDSESSION"sv << std::endl;
|
||||
std::raise(SIGINT);
|
||||
|
||||
// The signal handling is asynchronous, so we will wait here to be terminated.
|
||||
// If for some reason we don't terminate in a few seconds, Windows will kill us.
|
||||
SuspendThread(GetCurrentThread());
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
util::TaskPool::task_id_t force_shutdown = nullptr;
|
||||
|
||||
bool shutdown_by_interrupt = false;
|
||||
#ifdef _WIN32
|
||||
// Wait as long as possible to terminate Sunshine.exe during logoff/shutdown
|
||||
SetProcessShutdownParameters(0x100, SHUTDOWN_NORETRY);
|
||||
|
||||
auto exit_guard = util::fail_guard([&shutdown_by_interrupt, &force_shutdown]() {
|
||||
if(!shutdown_by_interrupt) {
|
||||
// We must create a hidden window to receive shutdown notifications since we load gdi32.dll
|
||||
std::thread window_thread([]() {
|
||||
WNDCLASSA wnd_class {};
|
||||
wnd_class.lpszClassName = "SunshineSessionMonitorClass";
|
||||
wnd_class.lpfnWndProc = SessionMonitorWindowProc;
|
||||
if(!RegisterClassA(&wnd_class)) {
|
||||
std::cout << "Failed to register session monitor window class"sv << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
task_pool.cancel(force_shutdown);
|
||||
auto wnd = CreateWindowExA(
|
||||
0,
|
||||
wnd_class.lpszClassName,
|
||||
"Sunshine Session Monitor Window",
|
||||
0,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr);
|
||||
if(!wnd) {
|
||||
std::cout << "Failed to create session monitor window"sv << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Sunshine exited: Press enter to continue"sv << std::endl;
|
||||
ShowWindow(wnd, SW_HIDE);
|
||||
|
||||
std::string _;
|
||||
std::getline(std::cin, _);
|
||||
// Run the message loop for our window
|
||||
MSG msg {};
|
||||
while(GetMessage(&msg, nullptr, 0, 0) > 0) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
});
|
||||
window_thread.detach();
|
||||
#endif
|
||||
|
||||
mail::man = std::make_shared<safe::mail_raw_t>();
|
||||
|
||||
@@ -171,6 +220,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
boost::shared_ptr<std::ostream> stream { &std::cout, NoDelete {} };
|
||||
sink->locked_backend()->add_stream(stream);
|
||||
sink->locked_backend()->add_stream(boost::make_shared<std::ofstream>(config::sunshine.log_file));
|
||||
sink->set_filter(severity >= config::sunshine.min_log_level);
|
||||
|
||||
sink->set_formatter([message = "Message"s, severity = "Severity"s](const bl::record_view &view, bl::formatting_ostream &os) {
|
||||
@@ -234,7 +284,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
// Create signal handler after logging has been initialized
|
||||
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
||||
on_signal(SIGINT, [&shutdown_by_interrupt, &force_shutdown, shutdown_event]() {
|
||||
on_signal(SIGINT, [&force_shutdown, shutdown_event]() {
|
||||
BOOST_LOG(info) << "Interrupt handler called"sv;
|
||||
|
||||
auto task = []() {
|
||||
@@ -244,7 +294,6 @@ int main(int argc, char *argv[]) {
|
||||
};
|
||||
force_shutdown = task_pool.pushDelayed(task, 10s).task_id;
|
||||
|
||||
shutdown_by_interrupt = true;
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
|
||||
@@ -308,6 +357,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
std::string read_file(const char *path) {
|
||||
if(!std::filesystem::exists(path)) {
|
||||
BOOST_LOG(debug) << "Missing file: " << path;
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifndef SUNSHINE_MAIN_H
|
||||
#define SUNSHINE_MAIN_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
#include "thread_pool.h"
|
||||
@@ -30,8 +31,10 @@ int write_file(const char *path, const std::string_view &contents);
|
||||
std::uint16_t map_port(int port);
|
||||
|
||||
namespace mail {
|
||||
#define MAIL(x) \
|
||||
constexpr auto x = std::string_view { #x }
|
||||
#define MAIL(x) \
|
||||
constexpr auto x = std::string_view { \
|
||||
#x \
|
||||
}
|
||||
|
||||
extern safe::mail_t man;
|
||||
|
||||
@@ -50,6 +53,4 @@ MAIL(idr);
|
||||
MAIL(rumble);
|
||||
#undef MAIL
|
||||
} // namespace mail
|
||||
|
||||
|
||||
#endif // SUNSHINE_MAIN_H
|
||||
|
||||
153
src/nvhttp.cpp
153
src/nvhttp.cpp
@@ -37,7 +37,69 @@ constexpr auto GFE_VERSION = "3.23.0.74";
|
||||
namespace fs = std::filesystem;
|
||||
namespace pt = boost::property_tree;
|
||||
|
||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||
class SunshineHttpsServer : public SimpleWeb::Server<SimpleWeb::HTTPS> {
|
||||
public:
|
||||
SunshineHttpsServer(const std::string &certification_file, const std::string &private_key_file)
|
||||
: SimpleWeb::Server<SimpleWeb::HTTPS>::Server(certification_file, private_key_file) {}
|
||||
|
||||
std::function<int(SSL *)> verify;
|
||||
std::function<void(std::shared_ptr<Response>, std::shared_ptr<Request>)> on_verify_failed;
|
||||
|
||||
protected:
|
||||
void after_bind() override {
|
||||
SimpleWeb::Server<SimpleWeb::HTTPS>::after_bind();
|
||||
|
||||
if(verify) {
|
||||
context.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once);
|
||||
context.set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
|
||||
// To respond with an error message, a connection must be established
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This is Server<HTTPS>::accept() with SSL validation support added
|
||||
void accept() override {
|
||||
auto connection = create_connection(*io_service, context);
|
||||
|
||||
acceptor->async_accept(connection->socket->lowest_layer(), [this, connection](const SimpleWeb::error_code &ec) {
|
||||
auto lock = connection->handler_runner->continue_lock();
|
||||
if(!lock)
|
||||
return;
|
||||
|
||||
if(ec != SimpleWeb::error::operation_aborted)
|
||||
this->accept();
|
||||
|
||||
auto session = std::make_shared<Session>(config.max_request_streambuf_size, connection);
|
||||
|
||||
if(!ec) {
|
||||
boost::asio::ip::tcp::no_delay option(true);
|
||||
SimpleWeb::error_code ec;
|
||||
session->connection->socket->lowest_layer().set_option(option, ec);
|
||||
|
||||
session->connection->set_timeout(config.timeout_request);
|
||||
session->connection->socket->async_handshake(boost::asio::ssl::stream_base::server, [this, session](const SimpleWeb::error_code &ec) {
|
||||
session->connection->cancel_timeout();
|
||||
auto lock = session->connection->handler_runner->continue_lock();
|
||||
if(!lock)
|
||||
return;
|
||||
if(!ec) {
|
||||
if(verify && !verify(session->connection->socket->native_handle()))
|
||||
this->write(session, on_verify_failed);
|
||||
else
|
||||
this->read(session);
|
||||
}
|
||||
else if(this->on_error)
|
||||
this->on_error(session->request, ec);
|
||||
});
|
||||
}
|
||||
else if(this->on_error)
|
||||
this->on_error(session->request, ec);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
using https_server_t = SunshineHttpsServer;
|
||||
using http_server_t = SimpleWeb::Server<SimpleWeb::HTTP>;
|
||||
|
||||
struct conf_intern_t {
|
||||
@@ -86,6 +148,14 @@ enum class op_e {
|
||||
REMOVE
|
||||
};
|
||||
|
||||
std::string get_arg(const args_t &args, const char *name) {
|
||||
auto it = args.find(name);
|
||||
if(it == std::end(args)) {
|
||||
throw std::out_of_range(name);
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void save_state() {
|
||||
pt::ptree root;
|
||||
|
||||
@@ -188,8 +258,8 @@ stream::launch_session_t make_launch_session(bool host_audio, const args_t &args
|
||||
stream::launch_session_t launch_session;
|
||||
|
||||
launch_session.host_audio = host_audio;
|
||||
launch_session.gcm_key = util::from_hex<crypto::aes_t>(args.at("rikey"s), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(args.at("rikeyid"s)));
|
||||
launch_session.gcm_key = util::from_hex<crypto::aes_t>(get_arg(args, "rikey"), true);
|
||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
|
||||
auto prepend_iv_p = (uint8_t *)&prepend_iv;
|
||||
|
||||
auto next = std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session.iv));
|
||||
@@ -217,7 +287,7 @@ void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
}
|
||||
void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto encrypted_response = util::from_hex_vec(args.at("serverchallengeresp"s), true);
|
||||
auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true);
|
||||
|
||||
std::vector<uint8_t> decrypted;
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
@@ -237,7 +307,7 @@ void serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &ar
|
||||
}
|
||||
|
||||
void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto challenge = util::from_hex_vec(args.at("clientchallenge"s), true);
|
||||
auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true);
|
||||
|
||||
crypto::cipher::ecb_t cipher(*sess.cipher_key, false);
|
||||
|
||||
@@ -274,7 +344,7 @@ void clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args)
|
||||
void clientpairingsecret(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, pair_session_t &sess, pt::ptree &tree, const args_t &args) {
|
||||
auto &client = sess.client;
|
||||
|
||||
auto pairingsecret = util::from_hex_vec(args.at("clientpairingsecret"), true);
|
||||
auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true);
|
||||
|
||||
std::string_view secret { pairingsecret.data(), 16 };
|
||||
std::string_view sign { pairingsecret.data() + secret.size(), crypto::digest_size };
|
||||
@@ -391,7 +461,7 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
return;
|
||||
}
|
||||
|
||||
auto uniqID { std::move(args.at("uniqueid"s)) };
|
||||
auto uniqID { std::move(get_arg(args, "uniqueid")) };
|
||||
auto sess_it = map_id_sess.find(uniqID);
|
||||
|
||||
args_t::const_iterator it;
|
||||
@@ -400,12 +470,12 @@ void pair(std::shared_ptr<safe::queue_t<crypto::x509_t>> &add_cert, std::shared_
|
||||
pair_session_t sess;
|
||||
|
||||
sess.client.uniqueID = std::move(uniqID);
|
||||
sess.client.cert = util::from_hex_vec(args.at("clientcert"s), true);
|
||||
sess.client.cert = util::from_hex_vec(get_arg(args, "clientcert"), true);
|
||||
|
||||
BOOST_LOG(debug) << sess.client.cert;
|
||||
auto ptr = map_id_sess.emplace(sess.client.uniqueID, std::move(sess)).first;
|
||||
|
||||
ptr->second.async_insert_pin.salt = std::move(args.at("salt"s));
|
||||
ptr->second.async_insert_pin.salt = std::move(get_arg(args, "salt"));
|
||||
|
||||
if(config::sunshine.flags[config::flag::PIN_STDIN]) {
|
||||
std::string pin;
|
||||
@@ -477,7 +547,7 @@ void pin(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> response,
|
||||
|
||||
response->close_connection_after_response = true;
|
||||
|
||||
auto address = request->remote_endpoint_address();
|
||||
auto address = request->remote_endpoint().address().to_string();
|
||||
auto ip_type = net::from_address(address);
|
||||
if(ip_type > http::origin_pin_allowed) {
|
||||
BOOST_LOG(info) << "/pin: ["sv << address << "] -- denied"sv;
|
||||
@@ -513,6 +583,8 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
}
|
||||
}
|
||||
|
||||
auto local_endpoint = request->local_endpoint();
|
||||
|
||||
pt::ptree tree;
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
@@ -523,9 +595,9 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
tree.put("root.uniqueid", http::unique_id);
|
||||
tree.put("root.HttpsPort", map_port(PORT_HTTPS));
|
||||
tree.put("root.ExternalPort", map_port(PORT_HTTP));
|
||||
tree.put("root.mac", platf::get_mac_address(request->local_endpoint_address()));
|
||||
tree.put("root.mac", platf::get_mac_address(local_endpoint.address().to_string()));
|
||||
tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 1 ? "1869449984" : "0");
|
||||
tree.put("root.LocalIP", request->local_endpoint_address());
|
||||
tree.put("root.LocalIP", local_endpoint.address().to_string());
|
||||
|
||||
if(config::video.hevc_mode == 3) {
|
||||
tree.put("root.ServerCodecModeSupport", "3843");
|
||||
@@ -568,8 +640,8 @@ void serverinfo(std::shared_ptr<typename SimpleWeb::ServerBase<T>::Response> res
|
||||
}
|
||||
auto current_appid = proc::proc.running();
|
||||
tree.put("root.PairStatus", pair_status);
|
||||
tree.put("root.currentgame", current_appid >= 0 ? current_appid + 1 : 0);
|
||||
tree.put("root.state", current_appid >= 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
|
||||
tree.put("root.currentgame", current_appid);
|
||||
tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE");
|
||||
|
||||
std::ostringstream data;
|
||||
|
||||
@@ -598,7 +670,7 @@ void applist(resp_https_t response, req_https_t request) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto clientID = args.at("uniqueid"s);
|
||||
auto clientID = get_arg(args, "uniqueid");
|
||||
|
||||
auto client = map_id_client.find(clientID);
|
||||
if(client == std::end(map_id_client)) {
|
||||
@@ -611,13 +683,12 @@ void applist(resp_https_t response, req_https_t request) {
|
||||
|
||||
apps.put("<xmlattr>.status_code", 200);
|
||||
|
||||
int x = 0;
|
||||
for(auto &proc : proc::proc.get_apps()) {
|
||||
pt::ptree app;
|
||||
|
||||
app.put("IsHdrSupported"s, config::video.hevc_mode == 3 ? 1 : 0);
|
||||
app.put("AppTitle"s, proc.name);
|
||||
app.put("ID"s, ++x);
|
||||
app.put("ID", proc.id);
|
||||
|
||||
apps.push_back(std::make_pair("App", std::move(app)));
|
||||
}
|
||||
@@ -655,17 +726,17 @@ void launch(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto appid = util::from_view(args.at("appid")) - 1;
|
||||
auto appid = util::from_view(get_arg(args, "appid"));
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
if(current_appid != -1) {
|
||||
if(current_appid > 0) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 400);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(appid >= 0) {
|
||||
if(appid > 0) {
|
||||
auto err = proc::proc.execute(appid);
|
||||
if(err) {
|
||||
tree.put("root.<xmlattr>.status_code", err);
|
||||
@@ -675,11 +746,11 @@ void launch(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
}
|
||||
}
|
||||
|
||||
host_audio = util::from_view(args.at("localAudioPlayMode"));
|
||||
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
|
||||
stream::launch_session_raise(make_launch_session(host_audio, args));
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
|
||||
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
|
||||
tree.put("root.gamesession", 1);
|
||||
}
|
||||
|
||||
@@ -705,7 +776,7 @@ void resume(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
}
|
||||
|
||||
auto current_appid = proc::proc.running();
|
||||
if(current_appid == -1) {
|
||||
if(current_appid == 0) {
|
||||
tree.put("root.resume", 0);
|
||||
tree.put("root.<xmlattr>.status_code", 503);
|
||||
|
||||
@@ -726,7 +797,7 @@ void resume(bool &host_audio, resp_https_t response, req_https_t request) {
|
||||
stream::launch_session_raise(make_launch_session(host_audio, args));
|
||||
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint_address() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
|
||||
tree.put("root.sessionUrl0", "rtsp://"s + request->local_endpoint().address().to_string() + ':' + std::to_string(map_port(stream::RTSP_SETUP_PORT)));
|
||||
tree.put("root.resume", 1);
|
||||
}
|
||||
|
||||
@@ -754,7 +825,7 @@ void cancel(resp_https_t response, req_https_t request) {
|
||||
tree.put("root.cancel", 1);
|
||||
tree.put("root.<xmlattr>.status_code", 200);
|
||||
|
||||
if(proc::proc.running() != -1) {
|
||||
if(proc::proc.running() > 0) {
|
||||
proc::proc.terminate();
|
||||
}
|
||||
}
|
||||
@@ -764,7 +835,7 @@ void appasset(resp_https_t response, req_https_t request) {
|
||||
print_req<SimpleWeb::HTTPS>(request);
|
||||
|
||||
auto args = request->parse_query_string();
|
||||
auto app_image = proc::proc.get_app_image(util::from_view(args.at("appid")));
|
||||
auto app_image = proc::proc.get_app_image(util::from_view(get_arg(args, "appid")));
|
||||
|
||||
std::ifstream in(app_image, std::ios::binary);
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
@@ -788,10 +859,6 @@ void start() {
|
||||
conf_intern.pkey = read_file(config::nvhttp.pkey.c_str());
|
||||
conf_intern.servercert = read_file(config::nvhttp.cert.c_str());
|
||||
|
||||
auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tls);
|
||||
ctx->use_certificate_chain_file(config::nvhttp.cert);
|
||||
ctx->use_private_key_file(config::nvhttp.pkey, boost::asio::ssl::context::pem);
|
||||
|
||||
crypto::cert_chain_t cert_chain;
|
||||
for(auto &[_, client] : map_id_client) {
|
||||
for(auto &cert : client.certs) {
|
||||
@@ -801,16 +868,11 @@ void start() {
|
||||
|
||||
auto add_cert = std::make_shared<safe::queue_t<crypto::x509_t>>(30);
|
||||
|
||||
ctx->set_verify_callback([](int verified, boost::asio::ssl::verify_context &ctx) {
|
||||
// To respond with an error message, a connection must be established
|
||||
return 1;
|
||||
});
|
||||
|
||||
// /resume doesn't get the parameter "localAudioPlayMode"
|
||||
// /launch will store it in host_audio
|
||||
bool host_audio {};
|
||||
|
||||
https_server_t https_server { ctx, boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_client_once };
|
||||
https_server_t https_server { config::nvhttp.cert, config::nvhttp.pkey };
|
||||
http_server_t http_server;
|
||||
|
||||
// Verify certificates after establishing connection
|
||||
@@ -870,7 +932,7 @@ void start() {
|
||||
tree.put("root.<xmlattr>.status_message"s, "The client is not authorized. Certificate verification failed."s);
|
||||
};
|
||||
|
||||
https_server.default_resource = not_found<SimpleWeb::HTTPS>;
|
||||
https_server.default_resource["GET"] = not_found<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTPS>;
|
||||
https_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTPS>(add_cert, resp, req); };
|
||||
https_server.resource["^/applist$"]["GET"] = applist;
|
||||
@@ -884,7 +946,7 @@ void start() {
|
||||
https_server.config.address = "0.0.0.0"s;
|
||||
https_server.config.port = port_https;
|
||||
|
||||
http_server.default_resource = not_found<SimpleWeb::HTTP>;
|
||||
http_server.default_resource["GET"] = not_found<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/serverinfo$"]["GET"] = serverinfo<SimpleWeb::HTTP>;
|
||||
http_server.resource["^/pair$"]["GET"] = [&add_cert](auto resp, auto req) { pair<SimpleWeb::HTTP>(add_cert, resp, req); };
|
||||
http_server.resource["^/pin/([0-9]+)$"]["GET"] = pin<SimpleWeb::HTTP>;
|
||||
@@ -893,20 +955,9 @@ void start() {
|
||||
http_server.config.address = "0.0.0.0"s;
|
||||
http_server.config.port = port_http;
|
||||
|
||||
try {
|
||||
https_server.bind();
|
||||
http_server.bind();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
BOOST_LOG(fatal) << "Couldn't bind http server to ports ["sv << port_http << ", "sv << port_http << "]: "sv << err.what();
|
||||
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
|
||||
auto accept_and_run = [&](auto *http_server) {
|
||||
try {
|
||||
http_server->accept_and_run();
|
||||
http_server->start();
|
||||
}
|
||||
catch(boost::system::system_error &err) {
|
||||
// It's possible the exception gets thrown after calling http_server->stop() from a different thread
|
||||
@@ -914,7 +965,7 @@ void start() {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(fatal) << "Couldn't start http server to ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
|
||||
BOOST_LOG(fatal) << "Couldn't start http server on ports ["sv << port_https << ", "sv << port_https << "]: "sv << err.what();
|
||||
shutdown_event->raise(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,27 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "src/main.h"
|
||||
#include "src/thread_safe.h"
|
||||
#include "src/utility.h"
|
||||
|
||||
struct sockaddr;
|
||||
struct AVFrame;
|
||||
|
||||
// Forward declarations of boost classes to avoid having to include boost headers
|
||||
// here, which results in issues with Windows.h and WinSock2.h include order.
|
||||
namespace boost {
|
||||
namespace filesystem {
|
||||
class path;
|
||||
}
|
||||
namespace process {
|
||||
class child;
|
||||
template<typename Char>
|
||||
class basic_environment;
|
||||
typedef basic_environment<char> environment;
|
||||
} // namespace process
|
||||
} // namespace boost
|
||||
|
||||
namespace platf {
|
||||
constexpr auto MAX_GAMEPADS = 32;
|
||||
|
||||
@@ -77,7 +92,6 @@ constexpr std::uint8_t map_surround71[] {
|
||||
FRONT_RIGHT,
|
||||
FRONT_CENTER,
|
||||
LOW_FREQUENCY,
|
||||
LOW_FREQUENCY,
|
||||
BACK_LEFT,
|
||||
BACK_RIGHT,
|
||||
SIDE_LEFT,
|
||||
@@ -143,9 +157,9 @@ struct img_t {
|
||||
public:
|
||||
img_t() = default;
|
||||
|
||||
img_t(img_t &&) = delete;
|
||||
img_t(const img_t &) = delete;
|
||||
img_t &operator=(img_t &&) = delete;
|
||||
img_t(img_t &&) = delete;
|
||||
img_t(const img_t &) = delete;
|
||||
img_t &operator=(img_t &&) = delete;
|
||||
img_t &operator=(const img_t &) = delete;
|
||||
|
||||
std::uint8_t *data {};
|
||||
@@ -183,7 +197,7 @@ struct hwdevice_t {
|
||||
* implementations must take ownership of 'frame'
|
||||
*/
|
||||
virtual int set_frame(AVFrame *frame) {
|
||||
std::abort(); // ^ This function must never be called
|
||||
BOOST_LOG(error) << "Illegal call to hwdevice_t::set_frame(). Did you forget to override it?";
|
||||
return -1;
|
||||
};
|
||||
|
||||
@@ -202,7 +216,8 @@ enum class capture_e : int {
|
||||
class display_t {
|
||||
public:
|
||||
/**
|
||||
* When display has a new image ready, this callback will be called with the new image.
|
||||
* When display has a new image ready or a timeout occurs, this callback will be called with the image.
|
||||
* If a frame was captured, frame_captured will be true. If a timeout occurred, it will be false.
|
||||
*
|
||||
* On Break Request -->
|
||||
* Returns nullptr
|
||||
@@ -211,7 +226,7 @@ public:
|
||||
* Returns the image object that should be filled next.
|
||||
* This may or may not be the image send with the callback
|
||||
*/
|
||||
using snapshot_cb_t = std::function<std::shared_ptr<img_t>(std::shared_ptr<img_t> &img)>;
|
||||
using snapshot_cb_t = std::function<std::shared_ptr<img_t>(std::shared_ptr<img_t> &img, bool frame_captured)>;
|
||||
|
||||
display_t() noexcept : offset_x { 0 }, offset_y { 0 } {}
|
||||
|
||||
@@ -289,6 +304,23 @@ std::shared_ptr<display_t> display(mem_type_e hwdevice_type, const std::string &
|
||||
// A list of names of displays accepted as display_name with the mem_type_e
|
||||
std::vector<std::string> display_names(mem_type_e hwdevice_type);
|
||||
|
||||
boost::process::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec);
|
||||
|
||||
enum class thread_priority_e : int {
|
||||
low,
|
||||
normal,
|
||||
high,
|
||||
critical
|
||||
};
|
||||
void adjust_thread_priority(thread_priority_e priority);
|
||||
|
||||
// Allow OS-specific actions to be taken to prepare for streaming
|
||||
void streaming_will_start();
|
||||
void streaming_will_stop();
|
||||
|
||||
bool restart_supported();
|
||||
bool restart();
|
||||
|
||||
input_t input();
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY);
|
||||
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y);
|
||||
|
||||
@@ -89,9 +89,7 @@ std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std
|
||||
if(!mic->mic) {
|
||||
auto err_str = pa_strerror(status);
|
||||
BOOST_LOG(error) << "pa_simple_new() failed: "sv << err_str;
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return mic;
|
||||
@@ -232,10 +230,8 @@ public:
|
||||
auto status = pa_mainloop_run(loop, &retval);
|
||||
|
||||
if(status < 0) {
|
||||
BOOST_LOG(fatal) << "Couldn't run pulseaudio main loop"sv;
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
BOOST_LOG(error) << "Couldn't run pulseaudio main loop"sv;
|
||||
return;
|
||||
}
|
||||
},
|
||||
loop.get()
|
||||
|
||||
@@ -505,10 +505,10 @@ public:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
|
||||
@@ -143,7 +143,7 @@ inline __device__ float2 calcUV(float3 pixel, const video::color_t *const color_
|
||||
float v = dot(pixel, make_float3(vec_v)) + vec_v.w;
|
||||
|
||||
u = u * color_matrix->range_uv.x + color_matrix->range_uv.y;
|
||||
v = (v * color_matrix->range_uv.x + color_matrix->range_uv.y) * 224.0f / 256.0f + 0.0625f;
|
||||
v = v * color_matrix->range_uv.x + color_matrix->range_uv.y;
|
||||
|
||||
return make_float2(u, v);
|
||||
}
|
||||
@@ -322,6 +322,8 @@ void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range)
|
||||
color_p = &video::colors[2];
|
||||
break;
|
||||
case 9: // SWS_CS_BT2020
|
||||
color_p = &video::colors[4];
|
||||
break;
|
||||
default:
|
||||
color_p = &video::colors[0];
|
||||
};
|
||||
|
||||
@@ -588,6 +588,8 @@ void sws_t::set_colorspace(std::uint32_t colorspace, std::uint32_t color_range)
|
||||
color_p = &video::colors[2];
|
||||
break;
|
||||
case 9: // SWS_CS_BT2020
|
||||
color_p = &video::colors[4];
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(warning) << "Colorspace: ["sv << colorspace << "] not yet supported: switching to default"sv;
|
||||
color_p = &video::colors[0];
|
||||
|
||||
@@ -35,7 +35,7 @@ class tex_t : public util::buffer_t<GLuint> {
|
||||
using util::buffer_t<GLuint>::buffer_t;
|
||||
|
||||
public:
|
||||
tex_t(tex_t &&) = default;
|
||||
tex_t(tex_t &&) = default;
|
||||
tex_t &operator=(tex_t &&) = default;
|
||||
|
||||
~tex_t();
|
||||
@@ -47,7 +47,7 @@ class frame_buf_t : public util::buffer_t<GLuint> {
|
||||
using util::buffer_t<GLuint>::buffer_t;
|
||||
|
||||
public:
|
||||
frame_buf_t(frame_buf_t &&) = default;
|
||||
frame_buf_t(frame_buf_t &&) = default;
|
||||
frame_buf_t &operator=(frame_buf_t &&) = default;
|
||||
|
||||
~frame_buf_t();
|
||||
|
||||
@@ -902,6 +902,9 @@ void broadcastRumble(safe::queue_t<mail_evdev_t> &rumble_queue_queue) {
|
||||
|
||||
void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y) {
|
||||
auto touchscreen = ((input_raw_t *)input.get())->touch_input.get();
|
||||
if(!touchscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto scaled_x = (int)std::lround((x + touch_port.offset_x) * ((float)target_touch_port.width / (float)touch_port.width));
|
||||
auto scaled_y = (int)std::lround((y + touch_port.offset_y) * ((float)target_touch_port.height / (float)touch_port.height));
|
||||
@@ -916,6 +919,9 @@ void abs_mouse(input_t &input, const touch_port_t &touch_port, float x, float y)
|
||||
|
||||
void move_mouse(input_t &input, int deltaX, int deltaY) {
|
||||
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
|
||||
if(!mouse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(deltaX) {
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_X, deltaX);
|
||||
@@ -954,6 +960,10 @@ void button_mouse(input_t &input, int button, bool release) {
|
||||
}
|
||||
|
||||
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
|
||||
if(!mouse) {
|
||||
return;
|
||||
}
|
||||
|
||||
libevdev_uinput_write_event(mouse, EV_MSC, MSC_SCAN, scan);
|
||||
libevdev_uinput_write_event(mouse, EV_KEY, btn_type, release ? 0 : 1);
|
||||
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
|
||||
@@ -963,6 +973,10 @@ void scroll(input_t &input, int high_res_distance) {
|
||||
int distance = high_res_distance / 120;
|
||||
|
||||
auto mouse = ((input_raw_t *)input.get())->mouse_input.get();
|
||||
if(!mouse) {
|
||||
return;
|
||||
}
|
||||
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL, distance);
|
||||
libevdev_uinput_write_event(mouse, EV_REL, REL_WHEEL_HI_RES, high_res_distance);
|
||||
libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0);
|
||||
@@ -978,6 +992,9 @@ static keycode_t keysym(std::uint16_t modcode) {
|
||||
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
auto keyboard = ((input_raw_t *)input.get())->keyboard_input.get();
|
||||
if(!keyboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto keycode = keysym(modcode);
|
||||
if(keycode.keycode == UNKNOWN) {
|
||||
@@ -1255,10 +1272,13 @@ input_t input() {
|
||||
gp.mouse_dev = mouse();
|
||||
gp.gamepad_dev = x360();
|
||||
|
||||
// If we do not have a keyboard, gamepad or mouse, no input is possible and we should abort
|
||||
if(gp.create_mouse() || gp.create_touchscreen() || gp.create_keyboard()) {
|
||||
log_flush();
|
||||
std::abort();
|
||||
gp.create_mouse();
|
||||
gp.create_touchscreen();
|
||||
gp.create_keyboard();
|
||||
|
||||
// If we do not have a keyboard, touchscreen, or mouse, no input is possible
|
||||
if(!gp.mouse_input && !gp.touch_input && !gp.keyboard_input) {
|
||||
BOOST_LOG(error) << "Unable to create any input devices! Are you a member of the 'input' group?"sv;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -251,11 +251,18 @@ public:
|
||||
|
||||
fb_t fb(plane_t::pointer plane) {
|
||||
cap_sys_admin admin;
|
||||
auto fb = drmModeGetFB2(fd.el, plane->fb_id);
|
||||
|
||||
auto fb2 = drmModeGetFB2(fd.el, plane->fb_id);
|
||||
if(fb2) {
|
||||
return std::make_unique<wrapper_fb>(fb2);
|
||||
}
|
||||
|
||||
auto fb = drmModeGetFB(fd.el, plane->fb_id);
|
||||
if(fb) {
|
||||
return std::make_unique<wrapper_fb>(fb);
|
||||
}
|
||||
return std::make_unique<wrapper_fb>(drmModeGetFB(fd.el, plane->fb_id));
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
crtc_t crtc(std::uint32_t id) {
|
||||
@@ -677,10 +684,10 @@ public:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
@@ -798,10 +805,10 @@ public:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "src/main.h"
|
||||
#include "src/platform/common.h"
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define SUNSHINE_GNUC_EXTENSION __extension__
|
||||
#else
|
||||
@@ -22,6 +24,7 @@
|
||||
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
namespace bp = boost::process;
|
||||
|
||||
window_system_e window_system;
|
||||
|
||||
@@ -140,6 +143,38 @@ std::string get_mac_address(const std::string_view &address) {
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) {
|
||||
BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
|
||||
if(!file) {
|
||||
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void adjust_thread_priority(thread_priority_e priority) {
|
||||
// Unimplemented
|
||||
}
|
||||
|
||||
void streaming_will_start() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
void streaming_will_stop() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
bool restart_supported() {
|
||||
// Restart not supported yet
|
||||
return false;
|
||||
}
|
||||
|
||||
bool restart() {
|
||||
// Restart not supported yet
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace source {
|
||||
enum source_e : std::size_t {
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
|
||||
@@ -39,7 +39,7 @@ public:
|
||||
dmabuf_t(const dmabuf_t &) = delete;
|
||||
|
||||
dmabuf_t &operator=(const dmabuf_t &) = delete;
|
||||
dmabuf_t &operator=(dmabuf_t &&) = delete;
|
||||
dmabuf_t &operator=(dmabuf_t &&) = delete;
|
||||
|
||||
dmabuf_t();
|
||||
|
||||
@@ -91,7 +91,7 @@ public:
|
||||
monitor_t(const monitor_t &) = delete;
|
||||
|
||||
monitor_t &operator=(const monitor_t &) = delete;
|
||||
monitor_t &operator=(monitor_t &&) = delete;
|
||||
monitor_t &operator=(monitor_t &&) = delete;
|
||||
|
||||
monitor_t(wl_output *output);
|
||||
|
||||
@@ -130,7 +130,7 @@ public:
|
||||
interface_t(const interface_t &) = delete;
|
||||
|
||||
interface_t &operator=(const interface_t &) = delete;
|
||||
interface_t &operator=(interface_t &&) = delete;
|
||||
interface_t &operator=(interface_t &&) = delete;
|
||||
|
||||
interface_t() noexcept;
|
||||
|
||||
@@ -193,7 +193,7 @@ public:
|
||||
monitor_t(const monitor_t &) = delete;
|
||||
|
||||
monitor_t &operator=(const monitor_t &) = delete;
|
||||
monitor_t &operator=(monitor_t &&) = delete;
|
||||
monitor_t &operator=(monitor_t &&) = delete;
|
||||
|
||||
monitor_t(wl_output *output);
|
||||
|
||||
|
||||
@@ -134,9 +134,10 @@ public:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
@@ -162,6 +163,12 @@ public:
|
||||
}
|
||||
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, (*rgb_opt)->tex[0]);
|
||||
|
||||
int w, h;
|
||||
gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w);
|
||||
gl::ctx.GetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &h);
|
||||
BOOST_LOG(debug) << "width and height: w "sv << w << ' h ' << h;
|
||||
|
||||
gl::ctx.GetTextureSubImage((*rgb_opt)->tex[0], 0, 0, 0, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out_base->height * img_out_base->row_pitch, img_out_base->data);
|
||||
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
@@ -233,9 +240,10 @@ public:
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
@@ -366,4 +374,4 @@ std::vector<std::string> wl_display_names() {
|
||||
return display_names;
|
||||
}
|
||||
|
||||
} // namespace platf
|
||||
} // namespace platf
|
||||
|
||||
@@ -476,10 +476,10 @@ struct x11_attr_t : public display_t {
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
@@ -587,10 +587,10 @@ struct shm_attr_t : public x11_attr_t {
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
img = snapshot_cb(img, false);
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
|
||||
@@ -60,11 +60,12 @@ struct av_display_t : public display_t {
|
||||
img_next->row_pitch = CVPixelBufferGetBytesPerRow(pixelBuffer);
|
||||
img_next->pixel_pitch = img_next->row_pitch / img_next->width;
|
||||
|
||||
img_next = snapshot_cb(img_next);
|
||||
img_next = snapshot_cb(img_next, true);
|
||||
|
||||
return img_next != nullptr;
|
||||
}];
|
||||
|
||||
// FIXME: We should time out if an image isn't returned for a while
|
||||
dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
|
||||
|
||||
return capture_e::ok;
|
||||
@@ -193,4 +194,4 @@ std::vector<std::string> display_names(mem_type_e hwdevice_type) {
|
||||
|
||||
return display_names;
|
||||
}
|
||||
}
|
||||
} // namespace platf
|
||||
|
||||
@@ -84,4 +84,4 @@ public:
|
||||
std::unique_ptr<audio_control_t> audio_control() {
|
||||
return std::make_unique<macos_audio_control_t>();
|
||||
}
|
||||
}
|
||||
} // namespace platf
|
||||
|
||||
@@ -9,10 +9,14 @@
|
||||
#include "src/main.h"
|
||||
#include "src/platform/common.h"
|
||||
|
||||
#include <boost/process.hpp>
|
||||
|
||||
using namespace std::literals;
|
||||
namespace fs = std::filesystem;
|
||||
namespace bp = boost::process;
|
||||
|
||||
namespace platf {
|
||||
|
||||
std::unique_ptr<deinit_t> init() {
|
||||
if(!CGPreflightScreenCaptureAccess()) {
|
||||
BOOST_LOG(error) << "No screen capture permission!"sv;
|
||||
@@ -116,6 +120,39 @@ std::string get_mac_address(const std::string_view &address) {
|
||||
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
||||
return "00:00:00:00:00:00"s;
|
||||
}
|
||||
|
||||
bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) {
|
||||
BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv;
|
||||
if(!file) {
|
||||
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void adjust_thread_priority(thread_priority_e priority) {
|
||||
// Unimplemented
|
||||
}
|
||||
|
||||
void streaming_will_start() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
void streaming_will_stop() {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
bool restart_supported() {
|
||||
// Restart not supported yet
|
||||
return false;
|
||||
}
|
||||
|
||||
bool restart() {
|
||||
// Restart not supported yet
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace platf
|
||||
|
||||
namespace dyn {
|
||||
|
||||
@@ -81,262 +81,10 @@ public:
|
||||
PROPVARIANT prop;
|
||||
};
|
||||
|
||||
class audio_pipe_t {
|
||||
public:
|
||||
static constexpr auto stereo = 2;
|
||||
static constexpr auto channels51 = 6;
|
||||
static constexpr auto channels71 = 8;
|
||||
|
||||
using samples_t = std::vector<std::int16_t>;
|
||||
using buf_t = util::buffer_t<std::int16_t>;
|
||||
|
||||
virtual void to_stereo(samples_t &out, const buf_t &in) = 0;
|
||||
virtual void to_51(samples_t &out, const buf_t &in) = 0;
|
||||
virtual void to_71(samples_t &out, const buf_t &in) = 0;
|
||||
};
|
||||
|
||||
class mono_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) override {
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end;) {
|
||||
*sample_out_p++ = *sample_in_pos * 7 / 10;
|
||||
*sample_out_p++ = *sample_in_pos++ * 7 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
|
||||
int left = *sample_in_pos++;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fl;
|
||||
sample_out_p[FRONT_CENTER] = fl * 6;
|
||||
sample_out_p[LOW_FREQUENCY] = fl / 10;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = left * 4 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
|
||||
int left = *sample_in_pos++;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fl;
|
||||
sample_out_p[FRONT_CENTER] = fl * 6;
|
||||
sample_out_p[LOW_FREQUENCY] = fl / 10;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = left * 4 / 10;
|
||||
sample_out_p[SIDE_LEFT] = left * 5 / 10;
|
||||
sample_out_p[SIDE_RIGHT] = left * 5 / 10;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class stereo_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) override {
|
||||
std::copy_n(std::begin(in), out.size(), std::begin(out));
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
|
||||
int left = sample_in_pos[speaker::FRONT_LEFT];
|
||||
int right = sample_in_pos[speaker::FRONT_RIGHT];
|
||||
|
||||
sample_in_pos += 2;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
auto fr = (right * 7 / 10);
|
||||
|
||||
auto mix = (fl + fr) / 2;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fr;
|
||||
sample_out_p[FRONT_CENTER] = mix;
|
||||
sample_out_p[LOW_FREQUENCY] = mix / 2;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = right * 4 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
|
||||
int left = sample_in_pos[speaker::FRONT_LEFT];
|
||||
int right = sample_in_pos[speaker::FRONT_RIGHT];
|
||||
|
||||
sample_in_pos += 2;
|
||||
|
||||
auto fl = (left * 7 / 10);
|
||||
auto fr = (right * 7 / 10);
|
||||
|
||||
auto mix = (fl + fr) / 2;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fr;
|
||||
sample_out_p[FRONT_CENTER] = mix;
|
||||
sample_out_p[LOW_FREQUENCY] = mix / 2;
|
||||
sample_out_p[BACK_LEFT] = left * 4 / 10;
|
||||
sample_out_p[BACK_RIGHT] = right * 4 / 10;
|
||||
sample_out_p[SIDE_LEFT] = left * 5 / 10;
|
||||
sample_out_p[SIDE_RIGHT] = right * 5 / 10;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class surr51_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
|
||||
int left {}, right {};
|
||||
|
||||
left += sample_in_pos[FRONT_LEFT];
|
||||
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
left += sample_in_pos[BACK_LEFT] * 7 / 10;
|
||||
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
|
||||
|
||||
right += sample_in_pos[FRONT_RIGHT];
|
||||
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
right += sample_in_pos[BACK_LEFT] * 3 / 10;
|
||||
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
|
||||
|
||||
sample_out_p[0] = left;
|
||||
sample_out_p[1] = right;
|
||||
|
||||
sample_in_pos += channels51;
|
||||
}
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
std::copy_n(std::begin(in), out.size(), std::begin(out));
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels71) {
|
||||
int fl = sample_in_pos[FRONT_LEFT];
|
||||
int fr = sample_in_pos[FRONT_RIGHT];
|
||||
int bl = sample_in_pos[BACK_LEFT];
|
||||
int br = sample_in_pos[BACK_RIGHT];
|
||||
|
||||
auto mix_l = (fl + bl) / 2;
|
||||
auto mix_r = (bl + br) / 2;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = fl;
|
||||
sample_out_p[FRONT_RIGHT] = fr;
|
||||
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
|
||||
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
|
||||
sample_out_p[BACK_LEFT] = bl;
|
||||
sample_out_p[BACK_RIGHT] = br;
|
||||
sample_out_p[SIDE_LEFT] = mix_l;
|
||||
sample_out_p[SIDE_RIGHT] = mix_r;
|
||||
|
||||
sample_in_pos += channels51;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class surr71_t : public audio_pipe_t {
|
||||
public:
|
||||
void to_stereo(samples_t &out, const buf_t &in) {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += stereo) {
|
||||
int left {}, right {};
|
||||
|
||||
left += sample_in_pos[FRONT_LEFT];
|
||||
left += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
left += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
left += sample_in_pos[BACK_LEFT] * 7 / 10;
|
||||
left += sample_in_pos[BACK_RIGHT] * 3 / 10;
|
||||
left += sample_in_pos[SIDE_LEFT];
|
||||
|
||||
right += sample_in_pos[FRONT_RIGHT];
|
||||
right += sample_in_pos[FRONT_CENTER] * 9 / 10;
|
||||
right += sample_in_pos[LOW_FREQUENCY] * 3 / 10;
|
||||
right += sample_in_pos[BACK_LEFT] * 3 / 10;
|
||||
right += sample_in_pos[BACK_RIGHT] * 7 / 10;
|
||||
right += sample_in_pos[SIDE_RIGHT];
|
||||
|
||||
sample_out_p[0] = left;
|
||||
sample_out_p[1] = right;
|
||||
|
||||
sample_in_pos += channels71;
|
||||
}
|
||||
}
|
||||
|
||||
void to_51(samples_t &out, const buf_t &in) override {
|
||||
using namespace speaker;
|
||||
|
||||
auto sample_in_pos = std::begin(in);
|
||||
auto sample_end = std::begin(out) + out.size();
|
||||
|
||||
for(auto sample_out_p = std::begin(out); sample_out_p != sample_end; sample_out_p += channels51) {
|
||||
auto sl = (int)sample_out_p[SIDE_LEFT] * 3 / 10;
|
||||
auto sr = (int)sample_out_p[SIDE_RIGHT] * 3 / 10;
|
||||
|
||||
sample_out_p[FRONT_LEFT] = sample_in_pos[FRONT_LEFT] + sl;
|
||||
sample_out_p[FRONT_RIGHT] = sample_in_pos[FRONT_RIGHT] + sr;
|
||||
sample_out_p[FRONT_CENTER] = sample_in_pos[FRONT_CENTER];
|
||||
sample_out_p[LOW_FREQUENCY] = sample_in_pos[LOW_FREQUENCY];
|
||||
sample_out_p[BACK_LEFT] = sample_in_pos[BACK_LEFT] + sl;
|
||||
sample_out_p[BACK_RIGHT] = sample_in_pos[BACK_RIGHT] + sr;
|
||||
|
||||
sample_in_pos += channels71;
|
||||
}
|
||||
}
|
||||
|
||||
void to_71(samples_t &out, const buf_t &in) override {
|
||||
std::copy_n(std::begin(in), out.size(), std::begin(out));
|
||||
}
|
||||
};
|
||||
|
||||
static std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
struct format_t {
|
||||
enum type_e : int {
|
||||
none,
|
||||
mono,
|
||||
stereo,
|
||||
surr51,
|
||||
surr71,
|
||||
@@ -346,12 +94,6 @@ struct format_t {
|
||||
int channels;
|
||||
int channel_mask;
|
||||
} formats[] {
|
||||
{
|
||||
format_t::mono,
|
||||
"Mono"sv,
|
||||
1,
|
||||
SPEAKER_FRONT_CENTER,
|
||||
},
|
||||
{
|
||||
format_t::stereo,
|
||||
"Stereo"sv,
|
||||
@@ -396,43 +138,53 @@ static format_t surround_51_side_speakers {
|
||||
SPEAKER_SIDE_RIGHT,
|
||||
};
|
||||
|
||||
void set_wave_format(audio::wave_format_t &wave_format, const format_t &format) {
|
||||
wave_format->nChannels = format.channels;
|
||||
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
|
||||
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
|
||||
WAVEFORMATEXTENSIBLE create_wave_format(const format_t &format) {
|
||||
WAVEFORMATEXTENSIBLE wave_format;
|
||||
|
||||
if(wave_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||||
((PWAVEFORMATEXTENSIBLE)wave_format.get())->dwChannelMask = format.channel_mask;
|
||||
}
|
||||
wave_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
||||
wave_format.Format.nChannels = format.channels;
|
||||
wave_format.Format.nSamplesPerSec = SAMPLE_RATE;
|
||||
wave_format.Format.wBitsPerSample = 16;
|
||||
wave_format.Format.nBlockAlign = wave_format.Format.nChannels * wave_format.Format.wBitsPerSample / 8;
|
||||
wave_format.Format.nAvgBytesPerSec = wave_format.Format.nSamplesPerSec * wave_format.Format.nBlockAlign;
|
||||
wave_format.Format.cbSize = sizeof(wave_format);
|
||||
|
||||
wave_format.Samples.wValidBitsPerSample = 16;
|
||||
wave_format.dwChannelMask = format.channel_mask;
|
||||
wave_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
|
||||
return wave_format;
|
||||
}
|
||||
|
||||
int init_wave_format(audio::wave_format_t &wave_format, DWORD sample_rate) {
|
||||
int set_wave_format(audio::wave_format_t &wave_format, const format_t &format) {
|
||||
wave_format->nSamplesPerSec = SAMPLE_RATE;
|
||||
wave_format->wBitsPerSample = 16;
|
||||
wave_format->nSamplesPerSec = sample_rate;
|
||||
|
||||
switch(wave_format->wFormatTag) {
|
||||
case WAVE_FORMAT_PCM:
|
||||
break;
|
||||
case WAVE_FORMAT_IEEE_FLOAT:
|
||||
break;
|
||||
case WAVE_FORMAT_EXTENSIBLE: {
|
||||
auto wave_ex = (PWAVEFORMATEXTENSIBLE)wave_format.get();
|
||||
if(IsEqualGUID(KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, wave_ex->SubFormat)) {
|
||||
wave_ex->Samples.wValidBitsPerSample = 16;
|
||||
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
break;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "Unsupported Sub Format for WAVE_FORMAT_EXTENSIBLE: [0x"sv << util::hex(wave_ex->SubFormat).to_string_view() << ']';
|
||||
auto wave_ex = (PWAVEFORMATEXTENSIBLE)wave_format.get();
|
||||
wave_ex->Samples.wValidBitsPerSample = 16;
|
||||
wave_ex->dwChannelMask = format.channel_mask;
|
||||
wave_ex->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
BOOST_LOG(error) << "Unsupported Wave Format: [0x"sv << util::hex(wave_format->wFormatTag).to_string_view() << ']';
|
||||
return -1;
|
||||
};
|
||||
|
||||
wave_format->nChannels = format.channels;
|
||||
wave_format->nBlockAlign = wave_format->nChannels * wave_format->wBitsPerSample / 8;
|
||||
wave_format->nAvgBytesPerSec = wave_format->nSamplesPerSec * wave_format->nBlockAlign;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
audio_client_t make_audio_client(device_t &device, const format_t &format, int sample_rate) {
|
||||
audio_client_t make_audio_client(device_t &device, const format_t &format) {
|
||||
audio_client_t audio_client;
|
||||
auto status = device->Activate(
|
||||
IID_IAudioClient,
|
||||
@@ -446,24 +198,14 @@ audio_client_t make_audio_client(device_t &device, const format_t &format, int s
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
wave_format_t wave_format;
|
||||
status = audio_client->GetMixFormat(&wave_format);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Couldn't acquire Wave Format [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(init_wave_format(wave_format, sample_rate)) {
|
||||
return nullptr;
|
||||
}
|
||||
set_wave_format(wave_format, format);
|
||||
WAVEFORMATEXTENSIBLE wave_format = create_wave_format(format);
|
||||
|
||||
status = audio_client->Initialize(
|
||||
AUDCLNT_SHAREMODE_SHARED,
|
||||
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
|
||||
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, // Enable automatic resampling to 48 KHz
|
||||
0, 0,
|
||||
wave_format.get(),
|
||||
(LPWAVEFORMATEX)&wave_format,
|
||||
nullptr);
|
||||
|
||||
if(status) {
|
||||
@@ -478,19 +220,21 @@ const wchar_t *no_null(const wchar_t *str) {
|
||||
return str ? str : L"Unknown";
|
||||
}
|
||||
|
||||
format_t::type_e validate_device(device_t &device, int sample_rate) {
|
||||
bool validate_device(device_t &device) {
|
||||
bool valid = false;
|
||||
|
||||
// Check for any valid format
|
||||
for(const auto &format : formats) {
|
||||
// Ensure WaveFromat is compatible
|
||||
auto audio_client = make_audio_client(device, format, sample_rate);
|
||||
auto audio_client = make_audio_client(device, format);
|
||||
|
||||
BOOST_LOG(debug) << format.name << ": "sv << (!audio_client ? "unsupported"sv : "supported"sv);
|
||||
|
||||
if(audio_client) {
|
||||
return format.type;
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
return format_t::none;
|
||||
return valid;
|
||||
}
|
||||
|
||||
device_t default_device(device_enum_t &device_enum) {
|
||||
@@ -514,32 +258,20 @@ device_t default_device(device_enum_t &device_enum) {
|
||||
class mic_wasapi_t : public mic_t {
|
||||
public:
|
||||
capture_e sample(std::vector<std::int16_t> &sample_out) override {
|
||||
auto sample_size = sample_out.size() / channels_out * channels_in;
|
||||
while(sample_buf_pos - std::begin(sample_buf) < sample_size) {
|
||||
//FIXME: Use IAudioClient3 instead of IAudioClient, that would allows for adjusting the latency of the audio samples
|
||||
auto capture_result = _fill_buffer();
|
||||
auto sample_size = sample_out.size();
|
||||
|
||||
// Refill the sample buffer if needed
|
||||
while(sample_buf_pos - std::begin(sample_buf) < sample_size) {
|
||||
auto capture_result = _fill_buffer();
|
||||
if(capture_result != capture_e::ok) {
|
||||
return capture_result;
|
||||
}
|
||||
}
|
||||
|
||||
switch(channels_out) {
|
||||
case 2:
|
||||
pipe->to_stereo(sample_out, sample_buf);
|
||||
break;
|
||||
case 6:
|
||||
pipe->to_51(sample_out, sample_buf);
|
||||
break;
|
||||
case 8:
|
||||
pipe->to_71(sample_out, sample_buf);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "converting to ["sv << channels_out << "] channels is not supported"sv;
|
||||
return capture_e::error;
|
||||
}
|
||||
// Fill the output buffer with samples
|
||||
std::copy_n(std::begin(sample_buf), sample_size, std::begin(sample_out));
|
||||
|
||||
// The excess samples should be in front of the queue
|
||||
// Move any excess samples to the front of the buffer
|
||||
std::move(&sample_buf[sample_size], sample_buf_pos, std::begin(sample_buf));
|
||||
sample_buf_pos -= sample_size;
|
||||
|
||||
@@ -576,31 +308,17 @@ public:
|
||||
}
|
||||
|
||||
for(auto &format : formats) {
|
||||
if(format.channels != channels_out) {
|
||||
BOOST_LOG(debug) << "Skipping audio format ["sv << format.name << "] with channel count ["sv << format.channels << " != "sv << channels_out << ']';
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Trying audio format ["sv << format.name << ']';
|
||||
audio_client = make_audio_client(device, format, sample_rate);
|
||||
audio_client = make_audio_client(device, format);
|
||||
|
||||
if(audio_client) {
|
||||
BOOST_LOG(debug) << "Found audio format ["sv << format.name << ']';
|
||||
channels_in = format.channels;
|
||||
this->channels_out = channels_out;
|
||||
|
||||
switch(channels_in) {
|
||||
case 1:
|
||||
pipe = std::make_unique<mono_t>();
|
||||
break;
|
||||
case 2:
|
||||
pipe = std::make_unique<stereo_t>();
|
||||
break;
|
||||
case 6:
|
||||
pipe = std::make_unique<surr51_t>();
|
||||
break;
|
||||
case 8:
|
||||
pipe = std::make_unique<surr71_t>();
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "converting from ["sv << channels_in << "] channels is not supported"sv;
|
||||
return -1;
|
||||
}
|
||||
channels = channels_out;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -623,7 +341,7 @@ public:
|
||||
}
|
||||
|
||||
// *2 --> needs to fit double
|
||||
sample_buf = util::buffer_t<std::int16_t> { std::max(frames, frame_size) * 2 * channels_in };
|
||||
sample_buf = util::buffer_t<std::int16_t> { std::max(frames, frame_size) * 2 * channels_out };
|
||||
sample_buf_pos = std::begin(sample_buf);
|
||||
|
||||
status = audio_client->GetService(IID_IAudioCaptureClient, (void **)&audio_capture);
|
||||
@@ -705,7 +423,7 @@ private:
|
||||
}
|
||||
|
||||
sample_aligned.uninitialized = std::end(sample_buf) - sample_buf_pos;
|
||||
auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels_in);
|
||||
auto n = std::min(sample_aligned.uninitialized, block_aligned.audio_sample_size * channels);
|
||||
|
||||
if(buffer_flags & AUDCLNT_BUFFERFLAGS_SILENT) {
|
||||
std::fill_n(sample_buf_pos, n, 0);
|
||||
@@ -742,13 +460,7 @@ public:
|
||||
|
||||
util::buffer_t<std::int16_t> sample_buf;
|
||||
std::int16_t *sample_buf_pos;
|
||||
|
||||
// out --> our audio output
|
||||
int channels_out;
|
||||
// in --> our wasapi input
|
||||
int channels_in;
|
||||
|
||||
std::unique_ptr<audio_pipe_t> pipe;
|
||||
int channels;
|
||||
};
|
||||
|
||||
class audio_control_t : public ::platf::audio_control_t {
|
||||
@@ -798,8 +510,7 @@ public:
|
||||
audio::device_t device;
|
||||
collection->Item(x, &device);
|
||||
|
||||
auto type = validate_device(device, SAMPLE_RATE);
|
||||
if(type == format_t::none) {
|
||||
if(!validate_device(device)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -897,9 +608,6 @@ public:
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if(init_wave_format(wave_format, SAMPLE_RATE)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
set_wave_format(wave_format, formats[(int)type - 1]);
|
||||
|
||||
WAVEFORMATEXTENSIBLE p {};
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
namespace platf::dxgi {
|
||||
extern const char *format_str[];
|
||||
|
||||
// Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime.
|
||||
// You should have a debugger like WinDbg attached to receive debug messages.
|
||||
auto constexpr D3D11_CREATE_DEVICE_FLAGS = D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
|
||||
|
||||
template<class T>
|
||||
void Release(T *dxgi) {
|
||||
dxgi->Release();
|
||||
@@ -27,14 +31,17 @@ using factory1_t = util::safe_ptr<IDXGIFactory1, Release<IDXGIFactory
|
||||
using dxgi_t = util::safe_ptr<IDXGIDevice, Release<IDXGIDevice>>;
|
||||
using dxgi1_t = util::safe_ptr<IDXGIDevice1, Release<IDXGIDevice1>>;
|
||||
using device_t = util::safe_ptr<ID3D11Device, Release<ID3D11Device>>;
|
||||
using device1_t = util::safe_ptr<ID3D11Device1, Release<ID3D11Device1>>;
|
||||
using device_ctx_t = util::safe_ptr<ID3D11DeviceContext, Release<ID3D11DeviceContext>>;
|
||||
using adapter_t = util::safe_ptr<IDXGIAdapter1, Release<IDXGIAdapter1>>;
|
||||
using output_t = util::safe_ptr<IDXGIOutput, Release<IDXGIOutput>>;
|
||||
using output1_t = util::safe_ptr<IDXGIOutput1, Release<IDXGIOutput1>>;
|
||||
using output5_t = util::safe_ptr<IDXGIOutput5, Release<IDXGIOutput5>>;
|
||||
using dup_t = util::safe_ptr<IDXGIOutputDuplication, Release<IDXGIOutputDuplication>>;
|
||||
using texture2d_t = util::safe_ptr<ID3D11Texture2D, Release<ID3D11Texture2D>>;
|
||||
using texture1d_t = util::safe_ptr<ID3D11Texture1D, Release<ID3D11Texture1D>>;
|
||||
using resource_t = util::safe_ptr<IDXGIResource, Release<IDXGIResource>>;
|
||||
using resource1_t = util::safe_ptr<IDXGIResource1, Release<IDXGIResource1>>;
|
||||
using multithread_t = util::safe_ptr<ID3D11Multithread, Release<ID3D11Multithread>>;
|
||||
using vs_t = util::safe_ptr<ID3D11VertexShader, Release<ID3D11VertexShader>>;
|
||||
using ps_t = util::safe_ptr<ID3D11PixelShader, Release<ID3D11PixelShader>>;
|
||||
@@ -48,6 +55,7 @@ using sampler_state_t = util::safe_ptr<ID3D11SamplerState, Release<ID3D11S
|
||||
using blob_t = util::safe_ptr<ID3DBlob, Release<ID3DBlob>>;
|
||||
using depth_stencil_state_t = util::safe_ptr<ID3D11DepthStencilState, Release<ID3D11DepthStencilState>>;
|
||||
using depth_stencil_view_t = util::safe_ptr<ID3D11DepthStencilView, Release<ID3D11DepthStencilView>>;
|
||||
using keyed_mutex_t = util::safe_ptr<IDXGIKeyedMutex, Release<IDXGIKeyedMutex>>;
|
||||
|
||||
namespace video {
|
||||
using device_t = util::safe_ptr<ID3D11VideoDevice, Release<ID3D11VideoDevice>>;
|
||||
@@ -118,7 +126,7 @@ public:
|
||||
device_ctx_t device_ctx;
|
||||
duplication_t dup;
|
||||
|
||||
DXGI_FORMAT format;
|
||||
DXGI_FORMAT capture_format;
|
||||
D3D_FEATURE_LEVEL feature_level;
|
||||
|
||||
typedef enum _D3DKMT_SCHEDULINGPRIORITYCLASS {
|
||||
@@ -131,6 +139,16 @@ public:
|
||||
} D3DKMT_SCHEDULINGPRIORITYCLASS;
|
||||
|
||||
typedef NTSTATUS WINAPI (*PD3DKMTSetProcessSchedulingPriorityClass)(HANDLE, D3DKMT_SCHEDULINGPRIORITYCLASS);
|
||||
|
||||
protected:
|
||||
int get_pixel_pitch() {
|
||||
return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4;
|
||||
}
|
||||
|
||||
const char *dxgi_format_to_string(DXGI_FORMAT format);
|
||||
|
||||
virtual int complete_img(img_t *img, bool dummy) = 0;
|
||||
virtual std::vector<DXGI_FORMAT> get_supported_sdr_capture_formats() = 0;
|
||||
};
|
||||
|
||||
class display_ram_t : public display_base_t {
|
||||
@@ -141,6 +159,8 @@ public:
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override;
|
||||
int dummy_img(img_t *img) override;
|
||||
int complete_img(img_t *img, bool dummy) override;
|
||||
std::vector<DXGI_FORMAT> get_supported_sdr_capture_formats() override;
|
||||
|
||||
int init(int framerate, const std::string &display_name);
|
||||
|
||||
@@ -156,6 +176,8 @@ public:
|
||||
|
||||
std::shared_ptr<img_t> alloc_img() override;
|
||||
int dummy_img(img_t *img_base) override;
|
||||
int complete_img(img_t *img_base, bool dummy) override;
|
||||
std::vector<DXGI_FORMAT> get_supported_sdr_capture_formats() override;
|
||||
|
||||
int init(int framerate, const std::string &display_name);
|
||||
|
||||
@@ -163,14 +185,17 @@ public:
|
||||
|
||||
sampler_state_t sampler_linear;
|
||||
|
||||
blend_t blend_enable;
|
||||
blend_t blend_alpha;
|
||||
blend_t blend_invert;
|
||||
blend_t blend_disable;
|
||||
|
||||
ps_t scene_ps;
|
||||
vs_t scene_vs;
|
||||
|
||||
texture2d_t src;
|
||||
gpu_cursor_t cursor;
|
||||
gpu_cursor_t cursor_alpha;
|
||||
gpu_cursor_t cursor_xor;
|
||||
|
||||
texture2d_t last_frame_copy;
|
||||
};
|
||||
} // namespace platf::dxgi
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <codecvt>
|
||||
#include <initguid.h>
|
||||
|
||||
#include "display.h"
|
||||
#include "misc.h"
|
||||
@@ -79,22 +80,21 @@ duplication_t::~duplication_t() {
|
||||
}
|
||||
|
||||
int display_base_t::init(int framerate, const std::string &display_name) {
|
||||
/* Uncomment when use of IDXGIOutput5 is implemented
|
||||
std::once_flag windows_cpp_once_flag;
|
||||
|
||||
std::call_once(windows_cpp_once_flag, []() {
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
const auto DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ((DPI_AWARENESS_CONTEXT)-4);
|
||||
|
||||
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
|
||||
|
||||
auto user32 = LoadLibraryA("user32.dll");
|
||||
auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
auto f = (User32_SetProcessDpiAwarenessContext)GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
if(f) {
|
||||
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
FreeLibrary(user32);
|
||||
});
|
||||
*/
|
||||
|
||||
// Ensure we can duplicate the current display
|
||||
syncThreadDesktop();
|
||||
@@ -186,7 +186,7 @@ int display_base_t::init(int framerate, const std::string &display_name) {
|
||||
adapter_p,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
nullptr,
|
||||
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
|
||||
D3D11_CREATE_DEVICE_FLAGS,
|
||||
featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),
|
||||
D3D11_SDK_VERSION,
|
||||
&device,
|
||||
@@ -272,7 +272,10 @@ int display_base_t::init(int framerate, const std::string &display_name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
dxgi->SetGPUThreadPriority(7);
|
||||
status = dxgi->SetGPUThreadPriority(7);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(warning) << "Failed to increase capture GPU thread priority. Please run application as administrator for optimal performance.";
|
||||
}
|
||||
}
|
||||
|
||||
// Try to reduce latency
|
||||
@@ -291,36 +294,68 @@ int display_base_t::init(int framerate, const std::string &display_name) {
|
||||
}
|
||||
|
||||
//FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD
|
||||
//TODO: Use IDXGIOutput5 for improved performance
|
||||
{
|
||||
dxgi::output1_t output1 {};
|
||||
status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We try this twice, in case we still get an error on reinitialization
|
||||
for(int x = 0; x < 2; ++x) {
|
||||
status = output1->DuplicateOutput((IUnknown *)device.get(), &dup.dup);
|
||||
if(SUCCEEDED(status)) {
|
||||
break;
|
||||
// IDXGIOutput5 is optional, but can provide improved performance and wide color support
|
||||
dxgi::output5_t output5 {};
|
||||
status = output->QueryInterface(IID_IDXGIOutput5, (void **)&output5);
|
||||
if(SUCCEEDED(status)) {
|
||||
// Ask the display implementation which formats it supports
|
||||
auto supported_formats = get_supported_sdr_capture_formats();
|
||||
if(supported_formats.empty()) {
|
||||
BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv;
|
||||
return -1;
|
||||
}
|
||||
std::this_thread::sleep_for(200ms);
|
||||
}
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
// We try this twice, in case we still get an error on reinitialization
|
||||
for(int x = 0; x < 2; ++x) {
|
||||
status = output5->DuplicateOutput1((IUnknown *)device.get(), 0, supported_formats.size(), supported_formats.data(), &dup.dup);
|
||||
if(SUCCEEDED(status)) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(200ms);
|
||||
}
|
||||
|
||||
// We don't retry with DuplicateOutput() because we can hit this codepath when we're racing
|
||||
// with mode changes and we don't want to accidentally fall back to suboptimal capture if
|
||||
// we get unlucky and succeed below.
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv;
|
||||
|
||||
dxgi::output1_t output1 {};
|
||||
status = output->QueryInterface(IID_IDXGIOutput1, (void **)&output1);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
for(int x = 0; x < 2; ++x) {
|
||||
status = output1->DuplicateOutput((IUnknown *)device.get(), &dup.dup);
|
||||
if(SUCCEEDED(status)) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(200ms);
|
||||
}
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DXGI_OUTDUPL_DESC dup_desc;
|
||||
dup.dup->GetDesc(&dup_desc);
|
||||
|
||||
format = dup_desc.ModeDesc.Format;
|
||||
BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']';
|
||||
BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']';
|
||||
|
||||
BOOST_LOG(debug) << "Source format ["sv << format_str[dup_desc.ModeDesc.Format] << ']';
|
||||
// Capture format will be determined from the first call to AcquireNextFrame()
|
||||
capture_format = DXGI_FORMAT_UNKNOWN;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -450,6 +485,10 @@ const char *format_str[] = {
|
||||
"DXGI_FORMAT_V408"
|
||||
};
|
||||
|
||||
const char *display_base_t::dxgi_format_to_string(DXGI_FORMAT format) {
|
||||
return format_str[format];
|
||||
}
|
||||
|
||||
} // namespace platf::dxgi
|
||||
|
||||
namespace platf {
|
||||
|
||||
@@ -181,10 +181,11 @@ capture_e display_ram_t::capture(snapshot_cb_t &&snapshot_cb, std::shared_ptr<::
|
||||
case platf::capture_e::error:
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
img = snapshot_cb(img, false);
|
||||
std::this_thread::sleep_for(1ms);
|
||||
continue;
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
img = snapshot_cb(img);
|
||||
img = snapshot_cb(img, true);
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int)status << ']';
|
||||
@@ -210,6 +211,14 @@ capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::millise
|
||||
return capture_status;
|
||||
}
|
||||
|
||||
const bool mouse_update_flag = frame_info.LastMouseUpdateTime.QuadPart != 0 || frame_info.PointerShapeBufferSize > 0;
|
||||
const bool frame_update_flag = frame_info.AccumulatedFrames != 0 || frame_info.LastPresentTime.QuadPart != 0;
|
||||
const bool update_flag = mouse_update_flag || frame_update_flag;
|
||||
|
||||
if(!update_flag) {
|
||||
return capture_e::timeout;
|
||||
}
|
||||
|
||||
if(frame_info.PointerShapeBufferSize > 0) {
|
||||
auto &img_data = cursor.img_data;
|
||||
|
||||
@@ -230,8 +239,7 @@ capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::millise
|
||||
cursor.visible = frame_info.PointerPosition.Visible;
|
||||
}
|
||||
|
||||
// If frame has been updated
|
||||
if(frame_info.LastPresentTime.QuadPart != 0) {
|
||||
if(frame_update_flag) {
|
||||
{
|
||||
texture2d_t src {};
|
||||
status = res->QueryInterface(IID_ID3D11Texture2D, (void **)&src);
|
||||
@@ -241,35 +249,82 @@ capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::millise
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
D3D11_TEXTURE2D_DESC desc;
|
||||
src->GetDesc(&desc);
|
||||
|
||||
// If we don't know the capture format yet, grab it from this texture and create the staging texture
|
||||
if(capture_format == DXGI_FORMAT_UNKNOWN) {
|
||||
capture_format = desc.Format;
|
||||
BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']';
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_STAGING;
|
||||
t.Format = capture_format;
|
||||
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
|
||||
auto status = device->CreateTexture2D(&t, nullptr, &texture);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return capture_e::error;
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible for our display enumeration to race with mode changes and result in
|
||||
// mismatched image pool and desktop texture sizes. If this happens, just reinit again.
|
||||
if(desc.Width != width || desc.Height != height) {
|
||||
BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']';
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
// It's also possible for the capture format to change on the fly. If that happens,
|
||||
// reinitialize capture to try format detection again and create new images.
|
||||
if(capture_format != desc.Format) {
|
||||
BOOST_LOG(info) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']';
|
||||
return capture_e::reinit;
|
||||
}
|
||||
|
||||
//Copy from GPU to CPU
|
||||
device_ctx->CopyResource(texture.get(), src.get());
|
||||
}
|
||||
}
|
||||
|
||||
if(img_info.pData) {
|
||||
device_ctx->Unmap(texture.get(), 0);
|
||||
img_info.pData = nullptr;
|
||||
// If we don't know the final capture format yet, encode a dummy image
|
||||
if(capture_format == DXGI_FORMAT_UNKNOWN) {
|
||||
BOOST_LOG(debug) << "Capture format is still unknown. Encoding a blank image"sv;
|
||||
|
||||
if(dummy_img(img)) {
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// Map the staging texture for CPU access (making it inaccessible for the GPU)
|
||||
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
// Now that we know the capture format, we can finish creating the image
|
||||
if(complete_img(img, false)) {
|
||||
device_ctx->Unmap(texture.get(), 0);
|
||||
img_info.pData = nullptr;
|
||||
return capture_e::error;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t *)img_info.pData, height * img_info.RowPitch, (std::uint8_t *)img->data);
|
||||
|
||||
// Unmap the staging texture to allow GPU access again
|
||||
device_ctx->Unmap(texture.get(), 0);
|
||||
img_info.pData = nullptr;
|
||||
}
|
||||
|
||||
const bool mouse_update =
|
||||
(frame_info.LastMouseUpdateTime.QuadPart || frame_info.PointerShapeBufferSize > 0) &&
|
||||
(cursor_visible && cursor.visible);
|
||||
|
||||
const bool update_flag = frame_info.LastPresentTime.QuadPart != 0 || mouse_update;
|
||||
|
||||
if(!update_flag) {
|
||||
return capture_e::timeout;
|
||||
}
|
||||
|
||||
std::copy_n((std::uint8_t *)img_info.pData, height * img_info.RowPitch, (std::uint8_t *)img->data);
|
||||
|
||||
if(cursor_visible && cursor.visible) {
|
||||
blend_cursor(cursor, *img);
|
||||
}
|
||||
@@ -280,48 +335,59 @@ capture_e display_ram_t::snapshot(::platf::img_t *img_base, std::chrono::millise
|
||||
std::shared_ptr<platf::img_t> display_ram_t::alloc_img() {
|
||||
auto img = std::make_shared<img_t>();
|
||||
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img_info.RowPitch;
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->data = new std::uint8_t[img->row_pitch * height];
|
||||
// Initialize fields that are format-independent
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
int display_ram_t::dummy_img(platf::img_t *img) {
|
||||
int display_ram_t::complete_img(platf::img_t *img, bool dummy) {
|
||||
// If this is not a dummy image, we must know the format by now
|
||||
if(!dummy && capture_format == DXGI_FORMAT_UNKNOWN) {
|
||||
BOOST_LOG(error) << "display_ram_t::complete_img() called with unknown capture format!";
|
||||
return -1;
|
||||
}
|
||||
|
||||
img->pixel_pitch = get_pixel_pitch();
|
||||
|
||||
if(dummy && !img->row_pitch) {
|
||||
// Assume our dummy image will have no padding
|
||||
img->row_pitch = img->pixel_pitch * img->width;
|
||||
}
|
||||
|
||||
// Reallocate the image buffer if the pitch changes
|
||||
if(!dummy && img->row_pitch != img_info.RowPitch) {
|
||||
img->row_pitch = img_info.RowPitch;
|
||||
delete img->data;
|
||||
img->data = nullptr;
|
||||
}
|
||||
|
||||
if(!img->data) {
|
||||
img->data = new std::uint8_t[img->row_pitch * height];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int display_ram_t::dummy_img(platf::img_t *img) {
|
||||
if(complete_img(img, true)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::fill_n((std::uint8_t *)img->data, height * img->row_pitch, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<DXGI_FORMAT> display_ram_t::get_supported_sdr_capture_formats() {
|
||||
return std::vector { DXGI_FORMAT_B8G8R8A8_UNORM };
|
||||
}
|
||||
|
||||
int display_ram_t::init(int framerate, const std::string &display_name) {
|
||||
if(display_base_t::init(framerate, display_name)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
D3D11_TEXTURE2D_DESC t {};
|
||||
t.Width = width;
|
||||
t.Height = height;
|
||||
t.MipLevels = 1;
|
||||
t.ArraySize = 1;
|
||||
t.SampleDesc.Count = 1;
|
||||
t.Usage = D3D11_USAGE_STAGING;
|
||||
t.Format = format;
|
||||
t.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
|
||||
|
||||
auto status = device->CreateTexture2D(&t, nullptr, &texture);
|
||||
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
// map the texture simply to get the pitch and stride
|
||||
status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info);
|
||||
if(FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to map the texture [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace platf::dxgi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -170,12 +170,31 @@ void CALLBACK ds4_notify(
|
||||
task_pool.push(&vigem_t::rumble, (vigem_t *)userdata, target, smallMotor, largeMotor);
|
||||
}
|
||||
|
||||
input_t input() {
|
||||
input_t result { new vigem_t {} };
|
||||
struct input_raw_t {
|
||||
~input_raw_t() {
|
||||
delete vigem;
|
||||
}
|
||||
|
||||
auto vigem = (vigem_t *)result.get();
|
||||
if(vigem->init()) {
|
||||
return nullptr;
|
||||
vigem_t *vigem;
|
||||
HKL keyboard_layout;
|
||||
};
|
||||
|
||||
input_t input() {
|
||||
input_t result { new input_raw_t {} };
|
||||
auto &raw = *(input_raw_t *)result.get();
|
||||
|
||||
raw.vigem = new vigem_t {};
|
||||
if(raw.vigem->init()) {
|
||||
delete raw.vigem;
|
||||
raw.vigem = nullptr;
|
||||
}
|
||||
|
||||
// Moonlight currently sends keys normalized to the US English layout.
|
||||
// We need to use that layout when converting to scancodes.
|
||||
raw.keyboard_layout = LoadKeyboardLayoutA("00000409", 0);
|
||||
if(!raw.keyboard_layout || LOWORD(raw.keyboard_layout) != 0x409) {
|
||||
BOOST_LOG(warning) << "Unable to load US English keyboard layout for scancode translation. Keyboard input may not work in games."sv;
|
||||
raw.keyboard_layout = NULL;
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -285,16 +304,23 @@ void scroll(input_t &input, int distance) {
|
||||
}
|
||||
|
||||
void keyboard(input_t &input, uint16_t modcode, bool release) {
|
||||
auto raw = (input_raw_t *)input.get();
|
||||
|
||||
INPUT i {};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
auto &ki = i.ki;
|
||||
|
||||
// For some reason, MapVirtualKey(VK_LWIN, MAPVK_VK_TO_VSC) doesn't seem to work :/
|
||||
if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE) {
|
||||
ki.wScan = MapVirtualKey(modcode, MAPVK_VK_TO_VSC);
|
||||
if(modcode != VK_LWIN && modcode != VK_RWIN && modcode != VK_PAUSE && raw->keyboard_layout != NULL) {
|
||||
ki.wScan = MapVirtualKeyEx(modcode, MAPVK_VK_TO_VSC, raw->keyboard_layout);
|
||||
}
|
||||
|
||||
// If we can map this to a scancode, send it as a scancode for maximum game compatibility.
|
||||
if(ki.wScan) {
|
||||
ki.dwFlags = KEYEVENTF_SCANCODE;
|
||||
}
|
||||
else {
|
||||
// If there is no scancode mapping, send it as a regular VK event.
|
||||
ki.wVk = modcode;
|
||||
}
|
||||
|
||||
@@ -355,19 +381,23 @@ void unicode(input_t &input, char *utf8, int size) {
|
||||
}
|
||||
|
||||
int alloc_gamepad(input_t &input, int nr, rumble_queue_t rumble_queue) {
|
||||
if(!input) {
|
||||
auto raw = (input_raw_t *)input.get();
|
||||
|
||||
if(!raw->vigem) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ((vigem_t *)input.get())->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad));
|
||||
return raw->vigem->alloc_gamepad_interal(nr, rumble_queue, map(config::input.gamepad));
|
||||
}
|
||||
|
||||
void free_gamepad(input_t &input, int nr) {
|
||||
if(!input) {
|
||||
auto raw = (input_raw_t *)input.get();
|
||||
|
||||
if(!raw->vigem) {
|
||||
return;
|
||||
}
|
||||
|
||||
((vigem_t *)input.get())->free_target(nr);
|
||||
raw->vigem->free_target(nr);
|
||||
}
|
||||
|
||||
static VIGEM_ERROR x360_update(client_t::pointer client, target_t::pointer gp, const gamepad_state_t &gamepad_state) {
|
||||
@@ -476,13 +506,13 @@ static VIGEM_ERROR ds4_update(client_t::pointer client, target_t::pointer gp, co
|
||||
|
||||
|
||||
void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
auto vigem = ((input_raw_t *)input.get())->vigem;
|
||||
|
||||
// If there is no gamepad support
|
||||
if(!input) {
|
||||
if(!vigem) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto vigem = (vigem_t *)input.get();
|
||||
|
||||
auto &[_, gp] = vigem->gamepads[nr];
|
||||
|
||||
VIGEM_ERROR status;
|
||||
@@ -495,17 +525,14 @@ void gamepad(input_t &input, int nr, const gamepad_state_t &gamepad_state) {
|
||||
}
|
||||
|
||||
if(!VIGEM_SUCCESS(status)) {
|
||||
BOOST_LOG(fatal) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
BOOST_LOG(warning) << "Couldn't send gamepad input to ViGEm ["sv << util::hex(status).to_string_view() << ']';
|
||||
}
|
||||
}
|
||||
|
||||
void freeInput(void *p) {
|
||||
auto vigem = (vigem_t *)p;
|
||||
auto input = (input_raw_t *)p;
|
||||
|
||||
delete vigem;
|
||||
delete input;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> &supported_gamepads() {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include <csignal>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/process.hpp>
|
||||
|
||||
// prevent clang format from "optimizing" the header include order
|
||||
// clang-format off
|
||||
@@ -10,17 +13,28 @@
|
||||
#include <windows.h>
|
||||
#include <winuser.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <userenv.h>
|
||||
#include <dwmapi.h>
|
||||
#include <timeapi.h>
|
||||
// clang-format on
|
||||
|
||||
#include "src/main.h"
|
||||
#include "src/platform/common.h"
|
||||
#include "src/utility.h"
|
||||
|
||||
namespace bp = boost::process;
|
||||
|
||||
using namespace std::literals;
|
||||
namespace platf {
|
||||
using adapteraddrs_t = util::c_ptr<IP_ADAPTER_ADDRESSES>;
|
||||
|
||||
bool enabled_mouse_keys = false;
|
||||
MOUSEKEYS previous_mouse_keys_state;
|
||||
|
||||
std::filesystem::path appdata() {
|
||||
return L"."sv;
|
||||
WCHAR sunshine_path[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, sunshine_path, _countof(sunshine_path));
|
||||
return std::filesystem::path { sunshine_path }.remove_filename() / L"config"sv;
|
||||
}
|
||||
|
||||
std::string from_sockaddr(const sockaddr *const socket_address) {
|
||||
@@ -120,4 +134,448 @@ void print_status(const std::string_view &prefix, HRESULT status) {
|
||||
|
||||
BOOST_LOG(error) << prefix << ": "sv << std::string_view { err_string, bytes };
|
||||
}
|
||||
|
||||
std::wstring utf8_to_wide_string(const std::string &str) {
|
||||
// Determine the size required for the destination string
|
||||
int chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), NULL, 0);
|
||||
|
||||
// Allocate it
|
||||
wchar_t buffer[chars] = {};
|
||||
|
||||
// Do the conversion for real
|
||||
chars = MultiByteToWideChar(CP_UTF8, 0, str.data(), str.length(), buffer, chars);
|
||||
return std::wstring(buffer, chars);
|
||||
}
|
||||
|
||||
std::string wide_to_utf8_string(const std::wstring &str) {
|
||||
// Determine the size required for the destination string
|
||||
int bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), NULL, 0, NULL, NULL);
|
||||
|
||||
// Allocate it
|
||||
char buffer[bytes] = {};
|
||||
|
||||
// Do the conversion for real
|
||||
bytes = WideCharToMultiByte(CP_UTF8, 0, str.data(), str.length(), buffer, bytes, NULL, NULL);
|
||||
return std::string(buffer, bytes);
|
||||
}
|
||||
|
||||
HANDLE duplicate_shell_token() {
|
||||
// Get the shell window (will usually be owned by explorer.exe)
|
||||
HWND shell_window = GetShellWindow();
|
||||
if(!shell_window) {
|
||||
BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Open a handle to the explorer.exe process
|
||||
DWORD shell_pid;
|
||||
GetWindowThreadProcessId(shell_window, &shell_pid);
|
||||
HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid);
|
||||
if(!shell_process) {
|
||||
BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Open explorer's token to clone for process creation
|
||||
HANDLE shell_token;
|
||||
BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token);
|
||||
CloseHandle(shell_process);
|
||||
if(!ret) {
|
||||
BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Duplicate the token to make it usable for process creation
|
||||
HANDLE new_token;
|
||||
ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token);
|
||||
CloseHandle(shell_token);
|
||||
if(!ret) {
|
||||
BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return new_token;
|
||||
}
|
||||
|
||||
PTOKEN_USER get_token_user(HANDLE token) {
|
||||
DWORD return_length;
|
||||
if(GetTokenInformation(token, TokenUser, NULL, 0, &return_length) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to get token information size: "sv << winerr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto user = (PTOKEN_USER)HeapAlloc(GetProcessHeap(), 0, return_length);
|
||||
if(!user) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(!GetTokenInformation(token, TokenUser, user, return_length, &return_length)) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to get token information: "sv << winerr;
|
||||
HeapFree(GetProcessHeap(), 0, user);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
void free_token_user(PTOKEN_USER user) {
|
||||
HeapFree(GetProcessHeap(), 0, user);
|
||||
}
|
||||
|
||||
bool is_token_same_user_as_process(HANDLE other_token) {
|
||||
HANDLE process_token;
|
||||
if(!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to open process token: "sv << winerr;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto process_user = get_token_user(process_token);
|
||||
CloseHandle(process_token);
|
||||
if(!process_user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto token_user = get_token_user(other_token);
|
||||
if(!token_user) {
|
||||
free_token_user(process_user);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = EqualSid(process_user->User.Sid, token_user->User.Sid);
|
||||
|
||||
free_token_user(process_user);
|
||||
free_token_user(token_user);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool merge_user_environment_block(bp::environment &env, HANDLE shell_token) {
|
||||
// Get the target user's environment block
|
||||
PVOID env_block;
|
||||
if(!CreateEnvironmentBlock(&env_block, shell_token, FALSE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the environment block and populate env
|
||||
for(auto c = (PWCHAR)env_block; *c != UNICODE_NULL; c += wcslen(c) + 1) {
|
||||
// Environment variable entries end with a null-terminator, so std::wstring() will get an entire entry.
|
||||
std::string env_tuple = wide_to_utf8_string(std::wstring { c });
|
||||
std::string env_name = env_tuple.substr(0, env_tuple.find('='));
|
||||
std::string env_val = env_tuple.substr(env_tuple.find('=') + 1);
|
||||
|
||||
// Perform a case-insensitive search to see if this variable name already exists
|
||||
auto itr = std::find_if(env.cbegin(), env.cend(),
|
||||
[&](const auto &e) { return boost::iequals(e.get_name(), env_name); });
|
||||
if(itr != env.cend()) {
|
||||
// Use this existing name if it is already present to ensure we merge properly
|
||||
env_name = itr->get_name();
|
||||
}
|
||||
|
||||
// For the PATH variable, we will merge the values together
|
||||
if(boost::iequals(env_name, "PATH")) {
|
||||
env[env_name] = env_val + ";" + env[env_name].to_string();
|
||||
}
|
||||
else {
|
||||
// Other variables will be superseded by those in the user's environment block
|
||||
env[env_name] = env_val;
|
||||
}
|
||||
}
|
||||
|
||||
DestroyEnvironmentBlock(env_block);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Note: This does NOT append a null terminator
|
||||
void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) {
|
||||
std::memcpy(&env_block[offset], wstr.data(), wstr.length() * sizeof(wchar_t));
|
||||
offset += wstr.length();
|
||||
}
|
||||
|
||||
std::wstring create_environment_block(bp::environment &env) {
|
||||
int size = 0;
|
||||
for(const auto &entry : env) {
|
||||
auto name = entry.get_name();
|
||||
auto value = entry.to_string();
|
||||
size += utf8_to_wide_string(name).length() + 1 /* L'=' */ + utf8_to_wide_string(value).length() + 1 /* L'\0' */;
|
||||
}
|
||||
|
||||
size += 1 /* L'\0' */;
|
||||
|
||||
wchar_t env_block[size];
|
||||
int offset = 0;
|
||||
for(const auto &entry : env) {
|
||||
auto name = entry.get_name();
|
||||
auto value = entry.to_string();
|
||||
|
||||
// Construct the NAME=VAL\0 string
|
||||
append_string_to_environment_block(env_block, offset, utf8_to_wide_string(name));
|
||||
env_block[offset++] = L'=';
|
||||
append_string_to_environment_block(env_block, offset, utf8_to_wide_string(value));
|
||||
env_block[offset++] = L'\0';
|
||||
}
|
||||
|
||||
// Append a final null terminator
|
||||
env_block[offset++] = L'\0';
|
||||
|
||||
return std::wstring(env_block, offset);
|
||||
}
|
||||
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST allocate_proc_thread_attr_list(DWORD attribute_count) {
|
||||
SIZE_T size;
|
||||
InitializeProcThreadAttributeList(NULL, attribute_count, 0, &size);
|
||||
|
||||
auto list = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, size);
|
||||
if(list == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if(!InitializeProcThreadAttributeList(list, attribute_count, 0, &size)) {
|
||||
HeapFree(GetProcessHeap(), 0, list);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
void free_proc_thread_attr_list(LPPROC_THREAD_ATTRIBUTE_LIST list) {
|
||||
DeleteProcThreadAttributeList(list);
|
||||
HeapFree(GetProcessHeap(), 0, list);
|
||||
}
|
||||
|
||||
bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec) {
|
||||
HANDLE shell_token = duplicate_shell_token();
|
||||
if(!shell_token) {
|
||||
// This can happen if the shell has crashed. Fail the launch rather than risking launching with
|
||||
// Sunshine's permissions unmodified.
|
||||
ec = std::make_error_code(std::errc::no_such_process);
|
||||
return bp::child();
|
||||
}
|
||||
|
||||
auto token_close = util::fail_guard([shell_token]() {
|
||||
CloseHandle(shell_token);
|
||||
});
|
||||
|
||||
// Populate env with user-specific environment variables
|
||||
if(!merge_user_environment_block(env, shell_token)) {
|
||||
ec = std::make_error_code(std::errc::not_enough_memory);
|
||||
return bp::child();
|
||||
}
|
||||
|
||||
// Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16
|
||||
std::wstring wcmd = utf8_to_wide_string(cmd);
|
||||
std::wstring env_block = create_environment_block(env);
|
||||
std::wstring start_dir = utf8_to_wide_string(working_dir.string());
|
||||
|
||||
STARTUPINFOEXW startup_info = {};
|
||||
startup_info.StartupInfo.cb = sizeof(startup_info);
|
||||
|
||||
// Allocate a process attribute list with space for 1 element
|
||||
startup_info.lpAttributeList = allocate_proc_thread_attr_list(1);
|
||||
if(startup_info.lpAttributeList == NULL) {
|
||||
ec = std::make_error_code(std::errc::not_enough_memory);
|
||||
return bp::child();
|
||||
}
|
||||
|
||||
auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() {
|
||||
free_proc_thread_attr_list(list);
|
||||
});
|
||||
|
||||
if(file) {
|
||||
HANDLE log_file_handle = (HANDLE)_get_osfhandle(_fileno(file));
|
||||
|
||||
// Populate std handles if the caller gave us a log file to use
|
||||
startup_info.StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
|
||||
startup_info.StartupInfo.hStdInput = NULL;
|
||||
startup_info.StartupInfo.hStdOutput = log_file_handle;
|
||||
startup_info.StartupInfo.hStdError = log_file_handle;
|
||||
|
||||
// Allow the log file handle to be inherited by the child process (without inheriting all of
|
||||
// our inheritable handles, such as our own log file handle created by SunshineSvc).
|
||||
UpdateProcThreadAttribute(startup_info.lpAttributeList,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
&log_file_handle,
|
||||
sizeof(log_file_handle),
|
||||
NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
// If we're running with the same user account as the shell, just use CreateProcess().
|
||||
// This will launch the child process elevated if Sunshine is elevated.
|
||||
PROCESS_INFORMATION process_info;
|
||||
BOOL ret;
|
||||
if(!is_token_same_user_as_process(shell_token)) {
|
||||
// Impersonate the user when launching the process. This will ensure that appropriate access
|
||||
// checks are done against the user token, not our SYSTEM token. It will also allow network
|
||||
// shares and mapped network drives to be used as launch targets, since those credentials
|
||||
// are stored per-user.
|
||||
if(!ImpersonateLoggedOnUser(shell_token)) {
|
||||
auto winerror = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror;
|
||||
ec = std::make_error_code(std::errc::permission_denied);
|
||||
return bp::child();
|
||||
}
|
||||
|
||||
// Launch the process with the duplicated shell token.
|
||||
// Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated.
|
||||
// Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified.
|
||||
ret = CreateProcessAsUserW(shell_token,
|
||||
NULL,
|
||||
(LPWSTR)wcmd.c_str(),
|
||||
NULL,
|
||||
NULL,
|
||||
!!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),
|
||||
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB,
|
||||
env_block.data(),
|
||||
start_dir.empty() ? NULL : start_dir.c_str(),
|
||||
(LPSTARTUPINFOW)&startup_info,
|
||||
&process_info);
|
||||
|
||||
// End impersonation of the logged on user. If this fails (which is extremely unlikely),
|
||||
// we will be running with an unknown user token. The only safe thing to do in that case
|
||||
// is terminate ourselves.
|
||||
if(!RevertToSelf()) {
|
||||
auto winerror = GetLastError();
|
||||
BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror;
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret = CreateProcessW(NULL,
|
||||
(LPWSTR)wcmd.c_str(),
|
||||
NULL,
|
||||
NULL,
|
||||
!!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES),
|
||||
EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB,
|
||||
env_block.data(),
|
||||
start_dir.empty() ? NULL : start_dir.c_str(),
|
||||
(LPSTARTUPINFOW)&startup_info,
|
||||
&process_info);
|
||||
}
|
||||
|
||||
if(ret) {
|
||||
// Since we are always spawning a process with a less privileged token than ourselves,
|
||||
// bp::child() should have no problem opening it with any access rights it wants.
|
||||
auto child = bp::child((bp::pid_t)process_info.dwProcessId);
|
||||
|
||||
// Only close handles after bp::child() has opened the process. If the process terminates
|
||||
// quickly, the PID could be reused if we close the process handle.
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(process_info.hProcess);
|
||||
|
||||
BOOST_LOG(info) << cmd << " running with PID "sv << child.id();
|
||||
return child;
|
||||
}
|
||||
else {
|
||||
// We must NOT try bp::child() here, since this case can potentially be induced by ACL
|
||||
// manipulation (denying yourself execute permission) to cause an escalation of privilege.
|
||||
auto winerror = GetLastError();
|
||||
BOOST_LOG(error) << "Failed to launch process: "sv << winerror;
|
||||
ec = std::make_error_code(std::errc::invalid_argument);
|
||||
return bp::child();
|
||||
}
|
||||
}
|
||||
|
||||
void adjust_thread_priority(thread_priority_e priority) {
|
||||
int win32_priority;
|
||||
|
||||
switch(priority) {
|
||||
case thread_priority_e::low:
|
||||
win32_priority = THREAD_PRIORITY_BELOW_NORMAL;
|
||||
break;
|
||||
case thread_priority_e::normal:
|
||||
win32_priority = THREAD_PRIORITY_NORMAL;
|
||||
break;
|
||||
case thread_priority_e::high:
|
||||
win32_priority = THREAD_PRIORITY_ABOVE_NORMAL;
|
||||
break;
|
||||
case thread_priority_e::critical:
|
||||
win32_priority = THREAD_PRIORITY_HIGHEST;
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "Unknown thread priority: "sv << (int)priority;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!SetThreadPriority(GetCurrentThread(), win32_priority)) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(warning) << "Unable to set thread priority to "sv << win32_priority << ": "sv << winerr;
|
||||
}
|
||||
}
|
||||
|
||||
void streaming_will_start() {
|
||||
// Enable MMCSS scheduling for DWM
|
||||
DwmEnableMMCSS(true);
|
||||
|
||||
// Reduce timer period to 1ms
|
||||
timeBeginPeriod(1);
|
||||
|
||||
// Promote ourselves to high priority class
|
||||
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
|
||||
|
||||
// If there is no mouse connected, enable Mouse Keys to force the cursor to appear
|
||||
if(!GetSystemMetrics(SM_MOUSEPRESENT)) {
|
||||
BOOST_LOG(info) << "A mouse was not detected. Sunshine will enable Mouse Keys while streaming to force the mouse cursor to appear.";
|
||||
|
||||
// Get the current state of Mouse Keys so we can restore it when streaming is over
|
||||
previous_mouse_keys_state.cbSize = sizeof(previous_mouse_keys_state);
|
||||
if(SystemParametersInfoW(SPI_GETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) {
|
||||
MOUSEKEYS new_mouse_keys_state = {};
|
||||
|
||||
// Enable Mouse Keys
|
||||
new_mouse_keys_state.cbSize = sizeof(new_mouse_keys_state);
|
||||
new_mouse_keys_state.dwFlags = MKF_MOUSEKEYSON | MKF_AVAILABLE;
|
||||
new_mouse_keys_state.iMaxSpeed = 10;
|
||||
new_mouse_keys_state.iTimeToMaxSpeed = 1000;
|
||||
if(SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &new_mouse_keys_state, 0)) {
|
||||
// Remember to restore the previous settings when we stop streaming
|
||||
enabled_mouse_keys = true;
|
||||
}
|
||||
else {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(warning) << "Unable to enable Mouse Keys: "sv << winerr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(warning) << "Unable to get current state of Mouse Keys: "sv << winerr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void streaming_will_stop() {
|
||||
// Demote ourselves back to normal priority class
|
||||
SetPriorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS);
|
||||
|
||||
// End our 1ms timer request
|
||||
timeEndPeriod(1);
|
||||
|
||||
// Disable MMCSS scheduling for DWM
|
||||
DwmEnableMMCSS(false);
|
||||
|
||||
// Restore Mouse Keys back to the previous settings if we turned it on
|
||||
if(enabled_mouse_keys) {
|
||||
enabled_mouse_keys = false;
|
||||
if(!SystemParametersInfoW(SPI_SETMOUSEKEYS, 0, &previous_mouse_keys_state, 0)) {
|
||||
auto winerr = GetLastError();
|
||||
BOOST_LOG(warning) << "Unable to restore original state of Mouse Keys: "sv << winerr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool restart_supported() {
|
||||
// Restart is supported if we're running from the service
|
||||
return (GetConsoleWindow() == NULL);
|
||||
}
|
||||
|
||||
bool restart() {
|
||||
// Raise SIGINT to trigger the graceful exit logic. The service will
|
||||
// restart us in a few seconds.
|
||||
std::raise(SIGINT);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace platf
|
||||
@@ -25,8 +25,9 @@ using namespace std::literals;
|
||||
#define SV(quote) __SV(quote)
|
||||
|
||||
extern "C" {
|
||||
constexpr auto DNS_REQUEST_PENDING = 9506L;
|
||||
|
||||
#ifndef __MINGW32__
|
||||
constexpr auto DNS_REQUEST_PENDING = 9506L;
|
||||
constexpr auto DNS_QUERY_REQUEST_VERSION1 = 0x1;
|
||||
constexpr auto DNS_QUERY_RESULTS_VERSION1 = 0x1;
|
||||
#endif
|
||||
@@ -88,26 +89,17 @@ _FN(_DnsServiceRegister, DWORD, (_In_ PDNS_SERVICE_REGISTER_REQUEST pRequest, _I
|
||||
|
||||
namespace platf::publish {
|
||||
VOID WINAPI register_cb(DWORD status, PVOID pQueryContext, PDNS_SERVICE_INSTANCE pInstance) {
|
||||
auto alarm = (safe::alarm_t<DNS_STATUS>::element_type *)pQueryContext;
|
||||
|
||||
auto fg = util::fail_guard([&]() {
|
||||
if(pInstance) {
|
||||
_DnsServiceFreeInstance(pInstance);
|
||||
}
|
||||
});
|
||||
auto alarm = (safe::alarm_t<PDNS_SERVICE_INSTANCE>::element_type *)pQueryContext;
|
||||
|
||||
if(status) {
|
||||
print_status("register_cb()"sv, status);
|
||||
alarm->ring(-1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
alarm->ring(0);
|
||||
alarm->ring(pInstance);
|
||||
}
|
||||
|
||||
static int service(bool enable) {
|
||||
auto alarm = safe::make_alarm<DNS_STATUS>();
|
||||
static int service(bool enable, PDNS_SERVICE_INSTANCE &existing_instance) {
|
||||
auto alarm = safe::make_alarm<PDNS_SERVICE_INSTANCE>();
|
||||
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
|
||||
@@ -124,38 +116,66 @@ static int service(bool enable) {
|
||||
DNS_SERVICE_REGISTER_REQUEST req {};
|
||||
req.Version = DNS_QUERY_REQUEST_VERSION1;
|
||||
req.pQueryContext = alarm.get();
|
||||
req.pServiceInstance = &instance;
|
||||
req.pServiceInstance = enable ? &instance : existing_instance;
|
||||
req.pRegisterCompletionCallback = register_cb;
|
||||
|
||||
DNS_STATUS status {};
|
||||
|
||||
if(enable) {
|
||||
status = _DnsServiceRegister(&req, nullptr);
|
||||
if(status != DNS_REQUEST_PENDING) {
|
||||
print_status("DnsServiceRegister()"sv, status);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
status = _DnsServiceDeRegister(&req, nullptr);
|
||||
if(status != DNS_REQUEST_PENDING) {
|
||||
print_status("DnsServiceDeRegister()"sv, status);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
alarm->wait();
|
||||
|
||||
status = *alarm->status();
|
||||
if(status) {
|
||||
BOOST_LOG(error) << "No mDNS service"sv;
|
||||
return -1;
|
||||
auto registered_instance = alarm->status();
|
||||
if(enable) {
|
||||
// Store this instance for later deregistration
|
||||
existing_instance = registered_instance;
|
||||
}
|
||||
else if(registered_instance) {
|
||||
// Deregistration was successful
|
||||
_DnsServiceFreeInstance(registered_instance);
|
||||
existing_instance = nullptr;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return registered_instance ? 0 : -1;
|
||||
}
|
||||
|
||||
class deinit_t : public ::platf::deinit_t {
|
||||
class mdns_registration_t : public ::platf::deinit_t {
|
||||
public:
|
||||
~deinit_t() override {
|
||||
if(service(false)) {
|
||||
std::abort();
|
||||
mdns_registration_t() : existing_instance(nullptr) {
|
||||
if(service(true, existing_instance)) {
|
||||
BOOST_LOG(error) << "Unable to register Sunshine mDNS service"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Unregistered Sunshine Gamestream service"sv;
|
||||
BOOST_LOG(info) << "Registered Sunshine mDNS service"sv;
|
||||
}
|
||||
|
||||
~mdns_registration_t() override {
|
||||
if(existing_instance) {
|
||||
if(service(false, existing_instance)) {
|
||||
BOOST_LOG(error) << "Unable to unregister Sunshine mDNS service"sv;
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Unregistered Sunshine mDNS service"sv;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
PDNS_SERVICE_INSTANCE existing_instance;
|
||||
};
|
||||
|
||||
int load_funcs(HMODULE handle) {
|
||||
@@ -184,12 +204,6 @@ std::unique_ptr<::platf::deinit_t> start() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(service(true)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Registered Sunshine Gamestream service"sv;
|
||||
|
||||
return std::make_unique<deinit_t>();
|
||||
return std::make_unique<mdns_registration_t>();
|
||||
}
|
||||
} // namespace platf::publish
|
||||
|
||||
337
src/process.cpp
337
src/process.cpp
@@ -9,13 +9,27 @@
|
||||
#include <vector>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/crc.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/program_options/parsers.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include "crypto.h"
|
||||
#include "main.h"
|
||||
#include "platform/common.h"
|
||||
#include "utility.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
// _SH constants for _wfsopen()
|
||||
#include <share.h>
|
||||
#endif
|
||||
|
||||
#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png"
|
||||
|
||||
namespace proc {
|
||||
using namespace std::literals;
|
||||
namespace bp = boost::process;
|
||||
@@ -35,7 +49,7 @@ void process_end(bp::child &proc, bp::group &proc_handle) {
|
||||
proc.wait();
|
||||
}
|
||||
|
||||
int exe(const std::string &cmd, bp::environment &env, file_t &file, std::error_code &ec) {
|
||||
int exe_with_full_privs(const std::string &cmd, bp::environment &env, file_t &file, std::error_code &ec) {
|
||||
if(!file) {
|
||||
return bp::system(cmd, env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
@@ -43,31 +57,69 @@ int exe(const std::string &cmd, bp::environment &env, file_t &file, std::error_c
|
||||
return bp::system(cmd, env, bp::std_out > file.get(), bp::std_err > file.get(), ec);
|
||||
}
|
||||
|
||||
boost::filesystem::path find_working_directory(const std::string &cmd, bp::environment &env) {
|
||||
// Parse the raw command string into parts to get the actual command portion
|
||||
#ifdef _WIN32
|
||||
auto parts = boost::program_options::split_winmain(cmd);
|
||||
#else
|
||||
auto parts = boost::program_options::split_unix(cmd);
|
||||
#endif
|
||||
if(parts.empty()) {
|
||||
BOOST_LOG(error) << "Unable to parse command: "sv << cmd;
|
||||
return boost::filesystem::path();
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Parsed executable ["sv << parts.at(0) << "] from command ["sv << cmd << ']';
|
||||
|
||||
// If the cmd path is not a complete path, resolve it using our PATH variable
|
||||
boost::filesystem::path cmd_path(parts.at(0));
|
||||
if(!cmd_path.is_complete()) {
|
||||
cmd_path = boost::process::search_path(parts.at(0));
|
||||
if(cmd_path.empty()) {
|
||||
BOOST_LOG(error) << "Unable to find executable ["sv << parts.at(0) << "]. Is it in your PATH?"sv;
|
||||
return boost::filesystem::path();
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Resolved executable ["sv << parts.at(0) << "] to path ["sv << cmd_path << ']';
|
||||
|
||||
// Now that we have a complete path, we can just use parent_path()
|
||||
return cmd_path.parent_path();
|
||||
}
|
||||
|
||||
int proc_t::execute(int app_id) {
|
||||
if(!running() && _app_id != -1) {
|
||||
// previous process exited on its own, reset _process_handle
|
||||
_process_handle = bp::group();
|
||||
|
||||
_app_id = -1;
|
||||
}
|
||||
|
||||
if(app_id < 0 || app_id >= _apps.size()) {
|
||||
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
|
||||
|
||||
return 404;
|
||||
}
|
||||
|
||||
// Ensure starting from a clean slate
|
||||
terminate();
|
||||
|
||||
auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {
|
||||
return app.id == std::to_string(app_id);
|
||||
});
|
||||
|
||||
if(iter == _apps.end()) {
|
||||
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
|
||||
return 404;
|
||||
}
|
||||
|
||||
_app_id = app_id;
|
||||
auto &proc = _apps[app_id];
|
||||
auto &proc = *iter;
|
||||
|
||||
_undo_begin = std::begin(proc.prep_cmds);
|
||||
_undo_it = _undo_begin;
|
||||
|
||||
if(!proc.output.empty() && proc.output != "null"sv) {
|
||||
#ifdef _WIN32
|
||||
// fopen() interprets the filename as an ANSI string on Windows, so we must convert it
|
||||
// to UTF-16 and use the wchar_t variants for proper Unicode log file path support.
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;
|
||||
auto woutput = converter.from_bytes(proc.output);
|
||||
|
||||
// Use _SH_DENYNO to allow us to open this log file again for writing even if it is
|
||||
// still open from a previous execution. This is required to handle the case of a
|
||||
// detached process executing again while the previous process is still running.
|
||||
_pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO));
|
||||
#else
|
||||
_pipe.reset(fopen(proc.output.c_str(), "a"));
|
||||
#endif
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
@@ -80,7 +132,7 @@ int proc_t::execute(int app_id) {
|
||||
auto &cmd = _undo_it->do_cmd;
|
||||
|
||||
BOOST_LOG(info) << "Executing: ["sv << cmd << ']';
|
||||
auto ret = exe(cmd, _env, _pipe, ec);
|
||||
auto ret = exe_with_full_privs(cmd, _env, _pipe, ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message();
|
||||
@@ -94,40 +146,35 @@ int proc_t::execute(int app_id) {
|
||||
}
|
||||
|
||||
for(auto &cmd : proc.detached) {
|
||||
BOOST_LOG(info) << "Spawning ["sv << cmd << ']';
|
||||
if(proc.output.empty() || proc.output == "null"sv) {
|
||||
bp::spawn(cmd, _env, bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
}
|
||||
else {
|
||||
bp::spawn(cmd, _env, bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
|
||||
}
|
||||
|
||||
boost::filesystem::path working_dir = proc.working_dir.empty() ?
|
||||
find_working_directory(cmd, _env) :
|
||||
boost::filesystem::path(proc.working_dir);
|
||||
BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']';
|
||||
auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec);
|
||||
if(ec) {
|
||||
BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message();
|
||||
}
|
||||
else {
|
||||
child.detach();
|
||||
}
|
||||
}
|
||||
|
||||
if(proc.cmd.empty()) {
|
||||
BOOST_LOG(debug) << "Executing [Desktop]"sv;
|
||||
BOOST_LOG(info) << "Executing [Desktop]"sv;
|
||||
placebo = true;
|
||||
}
|
||||
else {
|
||||
boost::filesystem::path working_dir = proc.working_dir.empty() ?
|
||||
boost::filesystem::path(proc.cmd).parent_path() :
|
||||
find_working_directory(proc.cmd, _env) :
|
||||
boost::filesystem::path(proc.working_dir);
|
||||
if(proc.output.empty() || proc.output == "null"sv) {
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << "] in ["sv << working_dir << ']';
|
||||
_process = platf::run_unprivileged(proc.cmd, working_dir, _env, _pipe.get(), ec);
|
||||
if(ec) {
|
||||
BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message();
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(info) << "Executing: ["sv << proc.cmd << ']';
|
||||
_process = bp::child(_process_handle, proc.cmd, _env, bp::start_dir(working_dir), bp::std_out > _pipe.get(), bp::std_err > _pipe.get(), ec);
|
||||
}
|
||||
}
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(warning) << "Couldn't run ["sv << proc.cmd << "]: System: "sv << ec.message();
|
||||
return -1;
|
||||
_process_handle.add(_process);
|
||||
}
|
||||
|
||||
fg.disable();
|
||||
@@ -140,7 +187,12 @@ int proc_t::running() {
|
||||
return _app_id;
|
||||
}
|
||||
|
||||
return -1;
|
||||
// Perform cleanup actions now if needed
|
||||
if(_process) {
|
||||
terminate();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void proc_t::terminate() {
|
||||
@@ -149,13 +201,9 @@ void proc_t::terminate() {
|
||||
// Ensure child process is terminated
|
||||
placebo = false;
|
||||
process_end(_process, _process_handle);
|
||||
_app_id = -1;
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(fatal) << "System: "sv << ec.message();
|
||||
log_flush();
|
||||
std::abort();
|
||||
}
|
||||
_process = bp::child();
|
||||
_process_handle = bp::group();
|
||||
_app_id = -1;
|
||||
|
||||
for(; _undo_it != _undo_begin; --_undo_it) {
|
||||
auto &cmd = (_undo_it - 1)->undo_cmd;
|
||||
@@ -164,20 +212,16 @@ void proc_t::terminate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Executing: ["sv << cmd << ']';
|
||||
BOOST_LOG(info) << "Executing: ["sv << cmd << ']';
|
||||
|
||||
auto ret = exe(cmd, _env, _pipe, ec);
|
||||
auto ret = exe_with_full_privs(cmd, _env, _pipe, ec);
|
||||
|
||||
if(ec) {
|
||||
BOOST_LOG(fatal) << "System: "sv << ec.message();
|
||||
log_flush();
|
||||
std::abort();
|
||||
BOOST_LOG(warning) << "System: "sv << ec.message();
|
||||
}
|
||||
|
||||
if(ret != 0) {
|
||||
BOOST_LOG(fatal) << "Return code ["sv << ret << ']';
|
||||
log_flush();
|
||||
std::abort();
|
||||
BOOST_LOG(warning) << "Return code ["sv << ret << ']';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,48 +240,12 @@ std::vector<ctx_t> &proc_t::get_apps() {
|
||||
// Returns default image if image configuration is not set.
|
||||
// Returns http content-type header compatible image type.
|
||||
std::string proc_t::get_app_image(int app_id) {
|
||||
auto app_index = app_id - 1;
|
||||
if(app_index < 0 || app_index >= _apps.size()) {
|
||||
BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']';
|
||||
return SUNSHINE_ASSETS_DIR "/box.png";
|
||||
}
|
||||
auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) {
|
||||
return app.id == std::to_string(app_id);
|
||||
});
|
||||
auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path;
|
||||
|
||||
auto default_image = SUNSHINE_ASSETS_DIR "/box.png";
|
||||
auto app_image_path = _apps[app_index].image_path;
|
||||
if(app_image_path.empty()) {
|
||||
// image is empty, return default box image
|
||||
return default_image;
|
||||
}
|
||||
|
||||
// get the image extension and convert it to lowercase
|
||||
auto image_extension = std::filesystem::path(app_image_path).extension().string();
|
||||
boost::to_lower(image_extension);
|
||||
|
||||
// return the default box image if extension is not "png"
|
||||
if(image_extension != ".png") {
|
||||
return default_image;
|
||||
}
|
||||
|
||||
// check if image is in assets directory
|
||||
auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path;
|
||||
if(std::filesystem::exists(full_image_path)) {
|
||||
return full_image_path.string();
|
||||
}
|
||||
else if(app_image_path == "./assets/steam.png") {
|
||||
// handle old default steam image definition
|
||||
return SUNSHINE_ASSETS_DIR "/steam.png";
|
||||
}
|
||||
|
||||
// check if specified image exists
|
||||
std::error_code code;
|
||||
if(!std::filesystem::exists(app_image_path, code)) {
|
||||
// return default box image if image does not exist
|
||||
return default_image;
|
||||
}
|
||||
|
||||
// image is a png, and not in assets directory
|
||||
// return only "content-type" http header compatible image type
|
||||
return app_image_path;
|
||||
return validate_app_image_path(app_image_path);
|
||||
}
|
||||
|
||||
proc_t::~proc_t() {
|
||||
@@ -279,8 +287,21 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v
|
||||
ss.write(pos, (dollar - pos));
|
||||
auto var_begin = next + 1;
|
||||
auto var_end = find_match(next, std::end(val_raw));
|
||||
auto var_name = std::string { var_begin, var_end };
|
||||
|
||||
ss << env[std::string { var_begin, var_end }].to_string();
|
||||
#ifdef _WIN32
|
||||
// Windows treats environment variable names in a case-insensitive manner,
|
||||
// so we look for a case-insensitive match here. This is critical for
|
||||
// correctly appending to PATH on Windows.
|
||||
auto itr = std::find_if(env.cbegin(), env.cend(),
|
||||
[&](const auto &e) { return boost::iequals(e.get_name(), var_name); });
|
||||
if(itr != env.cend()) {
|
||||
// Use an existing case-insensitive match
|
||||
var_name = itr->get_name();
|
||||
}
|
||||
#endif
|
||||
|
||||
ss << env[var_name].to_string();
|
||||
|
||||
pos = var_end + 1;
|
||||
next = var_end;
|
||||
@@ -306,6 +327,114 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string validate_app_image_path(std::string app_image_path) {
|
||||
if(app_image_path.empty()) {
|
||||
return DEFAULT_APP_IMAGE_PATH;
|
||||
}
|
||||
|
||||
// get the image extension and convert it to lowercase
|
||||
auto image_extension = std::filesystem::path(app_image_path).extension().string();
|
||||
boost::to_lower(image_extension);
|
||||
|
||||
// return the default box image if extension is not "png"
|
||||
if(image_extension != ".png") {
|
||||
return DEFAULT_APP_IMAGE_PATH;
|
||||
}
|
||||
|
||||
// check if image is in assets directory
|
||||
auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path;
|
||||
if(std::filesystem::exists(full_image_path)) {
|
||||
return full_image_path.string();
|
||||
}
|
||||
else if(app_image_path == "./assets/steam.png") {
|
||||
// handle old default steam image definition
|
||||
return SUNSHINE_ASSETS_DIR "/steam.png";
|
||||
}
|
||||
|
||||
// check if specified image exists
|
||||
std::error_code code;
|
||||
if(!std::filesystem::exists(app_image_path, code)) {
|
||||
// return default box image if image does not exist
|
||||
BOOST_LOG(warning) << "Couldn't find app image at path ["sv << app_image_path << ']';
|
||||
return DEFAULT_APP_IMAGE_PATH;
|
||||
}
|
||||
|
||||
// image is a png, and not in assets directory
|
||||
// return only "content-type" http header compatible image type
|
||||
return app_image_path;
|
||||
}
|
||||
|
||||
std::optional<std::string> calculate_sha256(const std::string &filename) {
|
||||
crypto::md_ctx_t ctx { EVP_MD_CTX_create() };
|
||||
if(!ctx) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if(!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Read file and update calculated SHA
|
||||
char buf[1024 * 16];
|
||||
std::ifstream file(filename, std::ifstream::binary);
|
||||
while(file.good()) {
|
||||
file.read(buf, sizeof(buf));
|
||||
if(!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
|
||||
unsigned char result[SHA256_DIGEST_LENGTH];
|
||||
if(!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Transform byte-array to string
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0');
|
||||
for(const auto &byte : result) {
|
||||
ss << std::setw(2) << (int)byte;
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
uint32_t calculate_crc32(const std::string &input) {
|
||||
boost::crc_32_type result;
|
||||
result.process_bytes(input.data(), input.length());
|
||||
return result.checksum();
|
||||
}
|
||||
|
||||
std::tuple<std::string, std::string> calculate_app_id(const std::string &app_name, std::string app_image_path, int index) {
|
||||
// Generate id by hashing name with image data if present
|
||||
std::vector<std::string> to_hash;
|
||||
to_hash.push_back(app_name);
|
||||
auto file_path = validate_app_image_path(app_image_path);
|
||||
if(file_path != DEFAULT_APP_IMAGE_PATH) {
|
||||
auto file_hash = calculate_sha256(file_path);
|
||||
if(file_hash) {
|
||||
to_hash.push_back(file_hash.value());
|
||||
}
|
||||
else {
|
||||
// Fallback to just hashing image path
|
||||
to_hash.push_back(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Create combined strings for hash
|
||||
std::stringstream ss;
|
||||
for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; });
|
||||
auto input_no_index = ss.str();
|
||||
ss << index;
|
||||
auto input_with_index = ss.str();
|
||||
|
||||
// CRC32 then truncate to signed 32-bit range due to client limitations
|
||||
auto id_no_index = std::to_string(abs((int32_t)calculate_crc32(input_no_index)));
|
||||
auto id_with_index = std::to_string(abs((int32_t)calculate_crc32(input_with_index)));
|
||||
|
||||
return std::make_tuple(id_no_index, id_with_index);
|
||||
}
|
||||
|
||||
std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
pt::ptree tree;
|
||||
|
||||
@@ -321,7 +450,9 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
|
||||
}
|
||||
|
||||
std::set<std::string> ids;
|
||||
std::vector<proc::ctx_t> apps;
|
||||
int i = 0;
|
||||
for(auto &[_, app_node] : apps_node) {
|
||||
proc::ctx_t ctx;
|
||||
|
||||
@@ -377,6 +508,17 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
|
||||
ctx.image_path = parse_env_val(this_env, *image_path);
|
||||
}
|
||||
|
||||
auto possible_ids = calculate_app_id(name, ctx.image_path, i++);
|
||||
if(ids.count(std::get<0>(possible_ids)) == 0) {
|
||||
// Avoid using index to generate id if possible
|
||||
ctx.id = std::get<0>(possible_ids);
|
||||
}
|
||||
else {
|
||||
// Fallback to include index on collision
|
||||
ctx.id = std::get<1>(possible_ids);
|
||||
}
|
||||
ids.insert(ctx.id);
|
||||
|
||||
ctx.name = std::move(name);
|
||||
ctx.prep_cmds = std::move(prep_cmds);
|
||||
ctx.detached = std::move(detached);
|
||||
@@ -399,11 +541,6 @@ void refresh(const std::string &file_name) {
|
||||
auto proc_opt = proc::parse(file_name);
|
||||
|
||||
if(proc_opt) {
|
||||
{
|
||||
proc::ctx_t ctx;
|
||||
ctx.name = "Desktop"s;
|
||||
proc_opt->get_apps().emplace(std::begin(proc_opt->get_apps()), std::move(ctx));
|
||||
}
|
||||
proc = std::move(*proc_opt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ struct ctx_t {
|
||||
std::string working_dir;
|
||||
std::string output;
|
||||
std::string image_path;
|
||||
std::string id;
|
||||
};
|
||||
|
||||
class proc_t {
|
||||
@@ -62,14 +63,14 @@ public:
|
||||
|
||||
proc_t(
|
||||
boost::process::environment &&env,
|
||||
std::vector<ctx_t> &&apps) : _app_id(-1),
|
||||
std::vector<ctx_t> &&apps) : _app_id(0),
|
||||
_env(std::move(env)),
|
||||
_apps(std::move(apps)) {}
|
||||
|
||||
int execute(int app_id);
|
||||
|
||||
/**
|
||||
* @return _app_id if a process is running, otherwise returns -1
|
||||
* @return _app_id if a process is running, otherwise returns 0
|
||||
*/
|
||||
int running();
|
||||
|
||||
@@ -98,6 +99,13 @@ private:
|
||||
std::vector<cmd_t>::const_iterator _undo_begin;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a stable id based on name and image data
|
||||
* @return tuple of id calculated without index (for use if no collision) and one with
|
||||
*/
|
||||
std::tuple<std::string, std::string> calculate_app_id(const std::string &app_name, std::string app_image_path, int index);
|
||||
|
||||
std::string validate_app_image_path(std::string app_image_path);
|
||||
void refresh(const std::string &file_name);
|
||||
std::optional<proc::proc_t> parse(const std::string &file_name);
|
||||
|
||||
|
||||
13
src/rtsp.cpp
13
src/rtsp.cpp
@@ -646,6 +646,19 @@ void cmd_announce(rtsp_server_t *server, tcp::socket &sock, msg_t &&req) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When using stereo audio, the audio quality is (strangely) indicated by whether the Host field
|
||||
// in the RTSP message matches a local interface's IP address. Fortunately, Moonlight always sends
|
||||
// 0.0.0.0 when it wants low quality, so it is easy to check without enumerating interfaces.
|
||||
if(config.audio.channels == 2) {
|
||||
for(auto option = req->options; option != nullptr; option = option->next) {
|
||||
if("Host"sv == option->option) {
|
||||
std::string_view content { option->content };
|
||||
BOOST_LOG(debug) << "Found Host: "sv << content;
|
||||
config.audio.flags[audio::config_t::HIGH_QUALITY] = (content.find("0.0.0.0"sv) == std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(config.monitor.videoFormat != 0 && config::video.hevc_mode == 1) {
|
||||
BOOST_LOG(warning) << "HEVC is disabled, yet the client requested HEVC"sv;
|
||||
|
||||
|
||||
@@ -65,6 +65,28 @@ enum class socket_e : int {
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct video_short_frame_header_t {
|
||||
uint8_t *payload() {
|
||||
return (uint8_t *)(this + 1);
|
||||
}
|
||||
|
||||
std::uint8_t headerType; // Always 0x01 for short headers
|
||||
std::uint8_t unknown[2];
|
||||
|
||||
// Currently known values:
|
||||
// 1 = Normal P-frame
|
||||
// 2 = IDR-frame
|
||||
// 4 = P-frame with intra-refresh blocks
|
||||
// 5 = P-frame after reference frame invalidation
|
||||
std::uint8_t frameType;
|
||||
|
||||
std::uint8_t unknown2[4];
|
||||
};
|
||||
|
||||
static_assert(
|
||||
sizeof(video_short_frame_header_t) == 8,
|
||||
"Short frame header must be 8 bytes");
|
||||
|
||||
struct video_packet_raw_t {
|
||||
uint8_t *payload() {
|
||||
return (uint8_t *)(this + 1);
|
||||
@@ -120,7 +142,7 @@ typedef struct control_encrypted_t {
|
||||
return (uint8_t *)(this + 1);
|
||||
}
|
||||
// encrypted control_header_v2 and payload data follow
|
||||
} * control_encrypted_p;
|
||||
} *control_encrypted_p;
|
||||
|
||||
struct audio_fec_packet_raw_t {
|
||||
uint8_t *payload() {
|
||||
@@ -718,6 +740,8 @@ void controlBroadcastThread(control_server_t *server) {
|
||||
input::passthrough(session->input, std::move(plaintext));
|
||||
});
|
||||
|
||||
// This thread handles latency-sensitive control messages
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::critical);
|
||||
|
||||
auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
|
||||
while(!shutdown_event->peek()) {
|
||||
@@ -754,7 +778,7 @@ void controlBroadcastThread(control_server_t *server) {
|
||||
})
|
||||
}
|
||||
|
||||
if(proc::proc.running() == -1) {
|
||||
if(proc::proc.running() == 0) {
|
||||
BOOST_LOG(debug) << "Process terminated"sv;
|
||||
|
||||
break;
|
||||
@@ -855,10 +879,8 @@ void recvThread(broadcast_ctx_t &ctx) {
|
||||
}
|
||||
|
||||
if(ec || !bytes) {
|
||||
BOOST_LOG(fatal) << "Couldn't receive data from udp socket: "sv << ec.message();
|
||||
|
||||
log_flush();
|
||||
std::abort();
|
||||
BOOST_LOG(error) << "Couldn't receive data from udp socket: "sv << ec.message();
|
||||
return;
|
||||
}
|
||||
|
||||
auto it = peer_to_session.find(peer.address());
|
||||
@@ -883,6 +905,10 @@ void recvThread(broadcast_ctx_t &ctx) {
|
||||
void videoBroadcastThread(udp::socket &sock) {
|
||||
auto shutdown_event = mail::man->event<bool>(mail::broadcast_shutdown);
|
||||
auto packets = mail::man->queue<video::packet_t>(mail::video_packets);
|
||||
auto timebase = boost::posix_time::microsec_clock::universal_time();
|
||||
|
||||
// Video traffic is sent on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::high);
|
||||
|
||||
while(auto packet = packets->pop()) {
|
||||
if(shutdown_event->peek()) {
|
||||
@@ -896,8 +922,11 @@ void videoBroadcastThread(udp::socket &sock) {
|
||||
std::string_view payload { (char *)av_packet->data, (size_t)av_packet->size };
|
||||
std::vector<uint8_t> payload_new;
|
||||
|
||||
auto nv_packet_header = "\0017charss"sv;
|
||||
std::copy(std::begin(nv_packet_header), std::end(nv_packet_header), std::back_inserter(payload_new));
|
||||
video_short_frame_header_t frame_header = {};
|
||||
frame_header.headerType = 0x01; // Short header type
|
||||
frame_header.frameType = (av_packet->flags & AV_PKT_FLAG_KEY) ? 2 : 1;
|
||||
|
||||
std::copy_n((uint8_t *)&frame_header, sizeof(frame_header), std::back_inserter(payload_new));
|
||||
std::copy(std::begin(payload), std::end(payload), std::back_inserter(payload_new));
|
||||
|
||||
payload = { (char *)payload_new.data(), payload_new.size() };
|
||||
@@ -992,6 +1021,10 @@ void videoBroadcastThread(udp::socket &sock) {
|
||||
for(auto x = 0; x < shards.size(); ++x) {
|
||||
auto *inspect = (video_packet_raw_t *)shards.data(x);
|
||||
|
||||
// RTP video timestamps use a 90 KHz clock
|
||||
auto now = boost::posix_time::microsec_clock::universal_time();
|
||||
auto timestamp = (now - timebase).total_microseconds() / (1000 / 90);
|
||||
|
||||
inspect->packet.fecInfo =
|
||||
(x << 12 |
|
||||
shards.data_shards << 22 |
|
||||
@@ -999,12 +1032,11 @@ void videoBroadcastThread(udp::socket &sock) {
|
||||
|
||||
inspect->rtp.header = 0x80 | FLAG_EXTENSION;
|
||||
inspect->rtp.sequenceNumber = util::endian::big<uint16_t>(lowseq + x);
|
||||
inspect->rtp.timestamp = util::endian::big<uint32_t>(timestamp);
|
||||
|
||||
inspect->packet.multiFecBlocks = (blockIndex << 4) | lastBlockIndex;
|
||||
inspect->packet.frameIndex = av_packet->pts;
|
||||
}
|
||||
|
||||
for(auto x = 0; x < shards.size(); ++x) {
|
||||
sock.send_to(asio::buffer(shards[x]), session->video.peer);
|
||||
}
|
||||
|
||||
@@ -1052,6 +1084,9 @@ void audioBroadcastThread(udp::socket &sock) {
|
||||
audio_packet->rtp.packetType = 97;
|
||||
audio_packet->rtp.ssrc = 0;
|
||||
|
||||
// Audio traffic is sent on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::high);
|
||||
|
||||
while(auto packet = packets->pop()) {
|
||||
if(shutdown_event->peek()) {
|
||||
break;
|
||||
@@ -1306,6 +1341,8 @@ void audioThread(session_t *session) {
|
||||
}
|
||||
|
||||
namespace session {
|
||||
std::atomic_uint running_sessions;
|
||||
|
||||
state_e state(session_t &session) {
|
||||
return session.state.load(std::memory_order_relaxed);
|
||||
}
|
||||
@@ -1322,6 +1359,20 @@ void stop(session_t &session) {
|
||||
}
|
||||
|
||||
void join(session_t &session) {
|
||||
// Current Nvidia drivers have a bug where NVENC can deadlock the encoder thread with hardware-accelerated
|
||||
// GPU scheduling enabled. If this happens, we will terminate ourselves and the service can restart.
|
||||
// The alternative is that Sunshine can never start another session until it's manually restarted.
|
||||
auto task = []() {
|
||||
BOOST_LOG(fatal) << "Hang detected! Session failed to terminate in 10 seconds."sv;
|
||||
log_flush();
|
||||
std::abort();
|
||||
};
|
||||
auto force_kill = task_pool.pushDelayed(task, 10s).task_id;
|
||||
auto fg = util::fail_guard([&force_kill]() {
|
||||
// Cancel the kill task if we manage to return from this function
|
||||
task_pool.cancel(force_kill);
|
||||
});
|
||||
|
||||
BOOST_LOG(debug) << "Waiting for video to end..."sv;
|
||||
session.videoThread.join();
|
||||
BOOST_LOG(debug) << "Waiting for audio to end..."sv;
|
||||
@@ -1367,6 +1418,11 @@ void join(session_t &session) {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the last session, invoke the platform callbacks
|
||||
if(--running_sessions == 0) {
|
||||
platf::streaming_will_stop();
|
||||
}
|
||||
|
||||
BOOST_LOG(debug) << "Session ended"sv;
|
||||
}
|
||||
|
||||
@@ -1405,6 +1461,11 @@ int start(session_t &session, const std::string &addr_string) {
|
||||
|
||||
session.state.store(state_e::RUNNING, std::memory_order_relaxed);
|
||||
|
||||
// If this is the first session, invoke the platform callbacks
|
||||
if(++running_sessions == 1) {
|
||||
platf::streaming_will_start();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include <miniupnpc/miniupnpc.h>
|
||||
#include <miniupnpc/upnpcommands.h>
|
||||
#include <miniupnpc.h>
|
||||
#include <upnpcommands.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "confighttp.h"
|
||||
@@ -82,6 +82,8 @@ static std::string_view status_string(int status) {
|
||||
case 1:
|
||||
return "Valid IGD device found"sv;
|
||||
case 2:
|
||||
return "Valid IGD device found, but it isn't connected"sv;
|
||||
case 3:
|
||||
return "A UPnP device has been found, but it wasn't recognized as an IGD"sv;
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ std::unique_ptr<platf::deinit_t> start() {
|
||||
IGDdatas data;
|
||||
|
||||
auto status = UPNP_GetValidIGD(device.get(), &urls.el, &data, lan_addr.data(), lan_addr.size());
|
||||
if(status != 1) {
|
||||
if(status != 1 && status != 2) {
|
||||
BOOST_LOG(error) << status_string(status);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ template<typename T>
|
||||
struct argument_type;
|
||||
|
||||
template<typename T, typename U>
|
||||
struct argument_type<T(U)> { typedef U type; };
|
||||
struct argument_type<T(U)> {
|
||||
typedef U type;
|
||||
};
|
||||
|
||||
#define KITTY_USING_MOVE_T(move_t, t, init_val, z) \
|
||||
class move_t { \
|
||||
@@ -58,22 +60,22 @@ struct argument_type<T(U)> { typedef U type; };
|
||||
}
|
||||
|
||||
#define KITTY_DECL_CONSTR(x) \
|
||||
x(x &&) noexcept = default; \
|
||||
x(x &&) noexcept = default; \
|
||||
x &operator=(x &&) noexcept = default; \
|
||||
x();
|
||||
|
||||
#define KITTY_DEFAULT_CONSTR_MOVE(x) \
|
||||
x(x &&) noexcept = default; \
|
||||
#define KITTY_DEFAULT_CONSTR_MOVE(x) \
|
||||
x(x &&) noexcept = default; \
|
||||
x &operator=(x &&) noexcept = default;
|
||||
|
||||
#define KITTY_DEFAULT_CONSTR_MOVE_THROW(x) \
|
||||
x(x &&) = default; \
|
||||
x(x &&) = default; \
|
||||
x &operator=(x &&) = default; \
|
||||
x() = default;
|
||||
|
||||
#define KITTY_DEFAULT_CONSTR(x) \
|
||||
KITTY_DEFAULT_CONSTR_MOVE(x) \
|
||||
x(const x &) noexcept = default; \
|
||||
#define KITTY_DEFAULT_CONSTR(x) \
|
||||
KITTY_DEFAULT_CONSTR_MOVE(x) \
|
||||
x(const x &) noexcept = default; \
|
||||
x &operator=(const x &) = default;
|
||||
|
||||
#define TUPLE_2D(a, b, expr) \
|
||||
@@ -133,7 +135,9 @@ template<bool V, class X, class Y>
|
||||
using either_t = typename __either<V, X, Y>::type;
|
||||
|
||||
template<class... Ts>
|
||||
struct overloaded : Ts... { using Ts::operator()...; };
|
||||
struct overloaded : Ts... {
|
||||
using Ts::operator()...;
|
||||
};
|
||||
template<class... Ts>
|
||||
overloaded(Ts...) -> overloaded<Ts...>;
|
||||
|
||||
@@ -453,7 +457,7 @@ public:
|
||||
constexpr uniq_ptr() noexcept : _p { nullptr } {}
|
||||
constexpr uniq_ptr(std::nullptr_t) noexcept : _p { nullptr } {}
|
||||
|
||||
uniq_ptr(const uniq_ptr &other) noexcept = delete;
|
||||
uniq_ptr(const uniq_ptr &other) noexcept = delete;
|
||||
uniq_ptr &operator=(const uniq_ptr &other) noexcept = delete;
|
||||
|
||||
template<class V>
|
||||
@@ -722,6 +726,9 @@ public:
|
||||
buffer_t(buffer_t &&o) noexcept : _els { o._els }, _buf { std::move(o._buf) } {
|
||||
o._els = 0;
|
||||
}
|
||||
buffer_t(const buffer_t &o) : _els { o._els }, _buf { std::make_unique<T[]>(_els) } {
|
||||
std::copy(o.begin(), o.end(), begin());
|
||||
}
|
||||
buffer_t &operator=(buffer_t &&o) noexcept {
|
||||
std::swap(_els, o._els);
|
||||
std::swap(_buf, o._buf);
|
||||
|
||||
272
src/video.cpp
272
src/video.cpp
@@ -286,12 +286,6 @@ struct encoder_t {
|
||||
option_t(std::string &&name, decltype(value) &&value) : name { std::move(name) }, value { std::move(value) } {}
|
||||
};
|
||||
|
||||
struct {
|
||||
int h264_high;
|
||||
int hevc_main;
|
||||
int hevc_main_10;
|
||||
} profile;
|
||||
|
||||
AVHWDeviceType dev_type;
|
||||
AVPixelFormat dev_pix_fmt;
|
||||
|
||||
@@ -299,7 +293,9 @@ struct encoder_t {
|
||||
AVPixelFormat dynamic_pix_fmt;
|
||||
|
||||
struct {
|
||||
std::vector<option_t> options;
|
||||
std::vector<option_t> common_options;
|
||||
std::vector<option_t> sdr_options;
|
||||
std::vector<option_t> hdr_options;
|
||||
std::optional<option_t> qp;
|
||||
|
||||
std::string name;
|
||||
@@ -407,7 +403,6 @@ auto capture_thread_sync = safe::make_shared<capture_thread_sync_ctx_t>(start_c
|
||||
|
||||
static encoder_t nvenc {
|
||||
"nvenc"sv,
|
||||
{ (int)nv::profile_h264_e::high, (int)nv::profile_hevc_e::main, (int)nv::profile_hevc_e::main_10 },
|
||||
#ifdef _WIN32
|
||||
AV_HWDEVICE_TYPE_D3D11VA,
|
||||
AV_PIX_FMT_D3D11,
|
||||
@@ -417,31 +412,48 @@ static encoder_t nvenc {
|
||||
#endif
|
||||
AV_PIX_FMT_NV12, AV_PIX_FMT_P010,
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "delay"s, 0 },
|
||||
{ "forced-idr"s, 1 },
|
||||
{ "zerolatency"s, 1 },
|
||||
{ "preset"s, &config::video.nv.preset },
|
||||
{ "tune"s, &config::video.nv.tune },
|
||||
{ "rc"s, &config::video.nv.rc },
|
||||
},
|
||||
// SDR-specific options
|
||||
{
|
||||
{ "profile"s, (int)nv::profile_hevc_e::main },
|
||||
},
|
||||
// HDR-specific options
|
||||
{
|
||||
{ "profile"s, (int)nv::profile_hevc_e::main_10 },
|
||||
},
|
||||
std::nullopt,
|
||||
"hevc_nvenc"s,
|
||||
},
|
||||
{
|
||||
{
|
||||
{ "delay"s, 0 },
|
||||
{ "forced-idr"s, 1 },
|
||||
{ "zerolatency"s, 1 },
|
||||
{ "preset"s, &config::video.nv.preset },
|
||||
{ "tune"s, &config::video.nv.tune },
|
||||
{ "rc"s, &config::video.nv.rc },
|
||||
{ "coder"s, &config::video.nv.coder },
|
||||
},
|
||||
// SDR-specific options
|
||||
{
|
||||
{ "profile"s, (int)nv::profile_h264_e::high },
|
||||
},
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>({ "qp"s, &config::video.qp }),
|
||||
"h264_nvenc"s,
|
||||
},
|
||||
PARALLEL_ENCODING,
|
||||
#ifdef _WIN32
|
||||
DEFAULT,
|
||||
dxgi_make_hwdevice_ctx
|
||||
#else
|
||||
PARALLEL_ENCODING,
|
||||
cuda_make_hwdevice_ctx
|
||||
#endif
|
||||
};
|
||||
@@ -449,39 +461,51 @@ static encoder_t nvenc {
|
||||
#ifdef _WIN32
|
||||
static encoder_t amdvce {
|
||||
"amdvce"sv,
|
||||
{ FF_PROFILE_H264_HIGH, FF_PROFILE_HEVC_MAIN },
|
||||
AV_HWDEVICE_TYPE_D3D11VA,
|
||||
AV_PIX_FMT_D3D11,
|
||||
AV_PIX_FMT_NV12, AV_PIX_FMT_P010,
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "enforce_hrd"s, true },
|
||||
{ "gops_per_idr"s, 1 },
|
||||
{ "header_insertion_mode"s, "idr"s },
|
||||
{ "gops_per_idr"s, 30 },
|
||||
{ "usage"s, "ultralowlatency"s },
|
||||
{ "quality"s, &config::video.amd.quality },
|
||||
{ "qmax"s, 51 },
|
||||
{ "qmin"s, 0 },
|
||||
{ "quality"s, &config::video.amd.quality_hevc },
|
||||
{ "rc"s, &config::video.amd.rc_hevc },
|
||||
{ "usage"s, "ultralowlatency"s },
|
||||
{ "vbaq"s, true },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>({ "qp_p"s, &config::video.qp }),
|
||||
"hevc_amf"s,
|
||||
},
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "usage"s, "ultralowlatency"s },
|
||||
{ "quality"s, &config::video.amd.quality },
|
||||
{ "rc"s, &config::video.amd.rc_h264 },
|
||||
{ "enforce_hrd"s, true },
|
||||
{ "log_to_dbg"s, "1"s },
|
||||
{ "qmax"s, 51 },
|
||||
{ "qmin"s, 0 },
|
||||
{ "quality"s, &config::video.amd.quality_h264 },
|
||||
{ "rc"s, &config::video.amd.rc_h264 },
|
||||
{ "usage"s, "ultralowlatency"s },
|
||||
{ "vbaq"s, true },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>({ "qp_p"s, &config::video.qp }),
|
||||
"h264_amf"s,
|
||||
},
|
||||
DEFAULT,
|
||||
PARALLEL_ENCODING,
|
||||
dxgi_make_hwdevice_ctx
|
||||
};
|
||||
#endif
|
||||
|
||||
static encoder_t software {
|
||||
"software"sv,
|
||||
{ FF_PROFILE_H264_HIGH, FF_PROFILE_HEVC_MAIN, FF_PROFILE_HEVC_MAIN_10 },
|
||||
AV_HWDEVICE_TYPE_NONE,
|
||||
AV_PIX_FMT_NONE,
|
||||
AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10,
|
||||
@@ -496,14 +520,19 @@ static encoder_t software {
|
||||
{ "preset"s, &config::video.sw.preset },
|
||||
{ "tune"s, &config::video.sw.tune },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
||||
"libx265"s,
|
||||
},
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "preset"s, &config::video.sw.preset },
|
||||
{ "tune"s, &config::video.sw.tune },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
||||
"libx264"s,
|
||||
},
|
||||
@@ -515,23 +544,30 @@ static encoder_t software {
|
||||
#ifdef __linux__
|
||||
static encoder_t vaapi {
|
||||
"vaapi"sv,
|
||||
{ FF_PROFILE_H264_HIGH, FF_PROFILE_HEVC_MAIN, FF_PROFILE_HEVC_MAIN_10 },
|
||||
AV_HWDEVICE_TYPE_VAAPI,
|
||||
AV_PIX_FMT_VAAPI,
|
||||
AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P10,
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "async_depth"s, 1 },
|
||||
{ "sei"s, 0 },
|
||||
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
||||
"hevc_vaapi"s,
|
||||
},
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "async_depth"s, 1 },
|
||||
{ "sei"s, 0 },
|
||||
{ "idr_interval"s, std::numeric_limits<int>::max() },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
|
||||
"h264_vaapi"s,
|
||||
},
|
||||
@@ -544,25 +580,30 @@ static encoder_t vaapi {
|
||||
#ifdef __APPLE__
|
||||
static encoder_t videotoolbox {
|
||||
"videotoolbox"sv,
|
||||
{ FF_PROFILE_H264_HIGH, FF_PROFILE_HEVC_MAIN, FF_PROFILE_HEVC_MAIN_10 },
|
||||
AV_HWDEVICE_TYPE_NONE,
|
||||
AV_PIX_FMT_VIDEOTOOLBOX,
|
||||
AV_PIX_FMT_NV12, AV_PIX_FMT_NV12,
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "allow_sw"s, &config::video.vt.allow_sw },
|
||||
{ "require_sw"s, &config::video.vt.require_sw },
|
||||
{ "realtime"s, &config::video.vt.realtime },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::nullopt,
|
||||
"hevc_videotoolbox"s,
|
||||
},
|
||||
{
|
||||
// Common options
|
||||
{
|
||||
{ "allow_sw"s, &config::video.vt.allow_sw },
|
||||
{ "require_sw"s, &config::video.vt.require_sw },
|
||||
{ "realtime"s, &config::video.vt.realtime },
|
||||
},
|
||||
{}, // SDR-specific options
|
||||
{}, // HDR-specific options
|
||||
std::nullopt,
|
||||
"h264_videotoolbox"s,
|
||||
},
|
||||
@@ -660,10 +701,13 @@ void captureThread(
|
||||
}
|
||||
}
|
||||
|
||||
// Capture takes place on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::critical);
|
||||
|
||||
while(capture_ctx_queue->running()) {
|
||||
bool artificial_reinit = false;
|
||||
|
||||
auto status = disp->capture([&](std::shared_ptr<platf::img_t> &img) -> std::shared_ptr<platf::img_t> {
|
||||
auto status = disp->capture([&](std::shared_ptr<platf::img_t> &img, bool frame_captured) -> std::shared_ptr<platf::img_t> {
|
||||
KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {
|
||||
if(!capture_ctx->images->running()) {
|
||||
capture_ctx = capture_ctxs.erase(capture_ctx);
|
||||
@@ -671,7 +715,10 @@ void captureThread(
|
||||
continue;
|
||||
}
|
||||
|
||||
capture_ctx->images->raise(img);
|
||||
if(frame_captured) {
|
||||
capture_ctx->images->raise(img);
|
||||
}
|
||||
|
||||
++capture_ctx;
|
||||
})
|
||||
|
||||
@@ -690,7 +737,10 @@ void captureThread(
|
||||
}
|
||||
|
||||
auto &next_img = *round_robin++;
|
||||
while(next_img.use_count() > 1) {}
|
||||
while(next_img.use_count() > 1) {
|
||||
// Sleep a bit to avoid starving the encoder threads
|
||||
std::this_thread::sleep_for(2ms);
|
||||
}
|
||||
|
||||
return next_img;
|
||||
},
|
||||
@@ -846,13 +896,13 @@ std::optional<session_t> make_session(const encoder_t &encoder, const config_t &
|
||||
ctx->framerate = AVRational { config.framerate, 1 };
|
||||
|
||||
if(config.videoFormat == 0) {
|
||||
ctx->profile = encoder.profile.h264_high;
|
||||
ctx->profile = FF_PROFILE_H264_HIGH;
|
||||
}
|
||||
else if(config.dynamicRange == 0) {
|
||||
ctx->profile = encoder.profile.hevc_main;
|
||||
ctx->profile = FF_PROFILE_HEVC_MAIN;
|
||||
}
|
||||
else {
|
||||
ctx->profile = encoder.profile.hevc_main_10;
|
||||
ctx->profile = FF_PROFILE_HEVC_MAIN_10;
|
||||
}
|
||||
|
||||
// B-frames delay decoder output, so never use them
|
||||
@@ -965,16 +1015,30 @@ std::optional<session_t> make_session(const encoder_t &encoder, const config_t &
|
||||
option.value);
|
||||
};
|
||||
|
||||
for(auto &option : video_format.options) {
|
||||
// Apply common options, then format-specific overrides
|
||||
for(auto &option : video_format.common_options) {
|
||||
handle_option(option);
|
||||
}
|
||||
for(auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) {
|
||||
handle_option(option);
|
||||
}
|
||||
|
||||
if(video_format[encoder_t::CBR]) {
|
||||
auto bitrate = config.bitrate * (hardware ? 1000 : 800); // software bitrate overshoots by ~20%
|
||||
ctx->rc_max_rate = bitrate;
|
||||
ctx->rc_buffer_size = bitrate / 10;
|
||||
ctx->bit_rate = bitrate;
|
||||
ctx->rc_min_rate = bitrate;
|
||||
auto bitrate = config.bitrate * 1000;
|
||||
ctx->rc_max_rate = bitrate;
|
||||
ctx->bit_rate = bitrate;
|
||||
ctx->rc_min_rate = bitrate;
|
||||
|
||||
if(!hardware && (ctx->slices > 1 || config.videoFormat != 0)) {
|
||||
// Use a larger rc_buffer_size for software encoding when slices are enabled,
|
||||
// because libx264 can severely degrade quality if the buffer is too small.
|
||||
// libx265 encounters this issue more frequently, so always scale the
|
||||
// buffer by 1.5x for software HEVC encoding.
|
||||
ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15);
|
||||
}
|
||||
else {
|
||||
ctx->rc_buffer_size = bitrate / config.framerate;
|
||||
}
|
||||
}
|
||||
else if(video_format.qp) {
|
||||
handle_option(*video_format.qp);
|
||||
@@ -1215,7 +1279,7 @@ encode_e encode_run_sync(
|
||||
|
||||
auto ec = platf::capture_e::ok;
|
||||
while(encode_session_ctx_queue.running()) {
|
||||
auto snapshot_cb = [&](std::shared_ptr<platf::img_t> &img) -> std::shared_ptr<platf::img_t> {
|
||||
auto snapshot_cb = [&](std::shared_ptr<platf::img_t> &img, bool frame_captured) -> std::shared_ptr<platf::img_t> {
|
||||
while(encode_session_ctx_queue.peek()) {
|
||||
auto encode_session_ctx = encode_session_ctx_queue.pop();
|
||||
if(!encode_session_ctx) {
|
||||
@@ -1259,7 +1323,7 @@ encode_e encode_run_sync(
|
||||
ctx->idr_events->pop();
|
||||
}
|
||||
|
||||
if(pos->session.device->convert(*img)) {
|
||||
if(frame_captured && pos->session.device->convert(*img)) {
|
||||
BOOST_LOG(error) << "Could not convert image"sv;
|
||||
ctx->shutdown_event->raise(true);
|
||||
|
||||
@@ -1320,7 +1384,10 @@ void captureThreadSync() {
|
||||
ctx.shutdown_event->raise(true);
|
||||
ctx.join_event->raise(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Encoding and capture takes place on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::high);
|
||||
|
||||
while(encode_run_sync(synced_session_ctxs, ctx) == encode_e::reinit) {}
|
||||
}
|
||||
@@ -1336,7 +1403,7 @@ void capture_async(
|
||||
auto lg = util::fail_guard([&]() {
|
||||
images->stop();
|
||||
shutdown_event->raise(true);
|
||||
});
|
||||
});
|
||||
|
||||
auto ref = capture_thread_async.ref();
|
||||
if(!ref) {
|
||||
@@ -1354,6 +1421,9 @@ void capture_async(
|
||||
|
||||
auto touch_port_event = mail->event<input::touch_port_t>(mail::touch_port);
|
||||
|
||||
// Encoding takes place on this thread
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::high);
|
||||
|
||||
while(!shutdown_event->peek() && images->running()) {
|
||||
// Wait for the main capture event when the display is being reinitialized
|
||||
if(ref->reinit_event.peek()) {
|
||||
@@ -1395,6 +1465,12 @@ void capture_async(
|
||||
std::move(hwdevice),
|
||||
ref->reinit_event, *ref->encoder_p,
|
||||
channel_data);
|
||||
|
||||
// Free images that weren't consumed by the encoder before it quit.
|
||||
// This is critical to allow the display_t to be freed correctly.
|
||||
while(images->peek()) {
|
||||
images->pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1620,36 +1696,92 @@ retry:
|
||||
}
|
||||
|
||||
int init() {
|
||||
bool encoder_found = false;
|
||||
if(!config::video.encoder.empty()) {
|
||||
// If there is a specific encoder specified, use it if it passes validation
|
||||
KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), {
|
||||
auto encoder = *pos;
|
||||
|
||||
if(encoder.name == config::video.encoder) {
|
||||
// Remove the encoder from the list entirely if it fails validation
|
||||
if(!validate_encoder(encoder)) {
|
||||
pos = encoders.erase(pos);
|
||||
break;
|
||||
}
|
||||
|
||||
// If we can't satisfy both the encoder and HDR requirement, prefer the encoder over HDR support
|
||||
if(config::video.hevc_mode == 3 && !encoder.hevc[encoder_t::DYNAMIC_RANGE]) {
|
||||
BOOST_LOG(warning) << "Encoder ["sv << config::video.encoder << "] does not support HDR on this system"sv;
|
||||
config::video.hevc_mode = 0;
|
||||
}
|
||||
|
||||
encoders.clear();
|
||||
encoders.emplace_back(encoder);
|
||||
encoder_found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
pos++;
|
||||
});
|
||||
|
||||
if(!encoder_found) {
|
||||
BOOST_LOG(error) << "Couldn't find any working encoder matching ["sv << config::video.encoder << ']';
|
||||
config::video.encoder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "// Testing for available encoders, this may generate errors. You can safely ignore those errors. //"sv;
|
||||
|
||||
KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), {
|
||||
if(
|
||||
(!config::video.encoder.empty() && pos->name != config::video.encoder) ||
|
||||
!validate_encoder(*pos) ||
|
||||
(config::video.hevc_mode == 3 && !pos->hevc[encoder_t::DYNAMIC_RANGE])) {
|
||||
pos = encoders.erase(pos);
|
||||
// If we haven't found an encoder yet but we want one with HDR support, search for that now.
|
||||
if(!encoder_found && config::video.hevc_mode == 3) {
|
||||
KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), {
|
||||
auto encoder = *pos;
|
||||
|
||||
continue;
|
||||
// Remove the encoder from the list entirely if it fails validation
|
||||
if(!validate_encoder(encoder)) {
|
||||
pos = encoders.erase(pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip it if it doesn't support HDR
|
||||
if(!encoder.hevc[encoder_t::DYNAMIC_RANGE]) {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
encoders.clear();
|
||||
encoders.emplace_back(encoder);
|
||||
encoder_found = true;
|
||||
break;
|
||||
});
|
||||
|
||||
if(!encoder_found) {
|
||||
BOOST_LOG(error) << "Couldn't find any working HDR-capable encoder"sv;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
})
|
||||
// If no encoder was specified or the specified encoder was unusable, keep trying
|
||||
// the remaining encoders until we find one that passes validation.
|
||||
if(!encoder_found) {
|
||||
KITTY_WHILE_LOOP(auto pos = std::begin(encoders), pos != std::end(encoders), {
|
||||
if(!validate_encoder(*pos)) {
|
||||
pos = encoders.erase(pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
});
|
||||
}
|
||||
|
||||
if(encoders.empty()) {
|
||||
BOOST_LOG(fatal) << "Couldn't find any working encoder"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
BOOST_LOG(info);
|
||||
BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv;
|
||||
BOOST_LOG(info);
|
||||
|
||||
if(encoders.empty()) {
|
||||
if(config::video.encoder.empty()) {
|
||||
BOOST_LOG(fatal) << "Couldn't find any encoder"sv;
|
||||
}
|
||||
else {
|
||||
BOOST_LOG(fatal) << "Couldn't find any encoder matching ["sv << config::video.encoder << ']';
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto &encoder = encoders.front();
|
||||
|
||||
BOOST_LOG(debug) << "------ h264 ------"sv;
|
||||
@@ -1834,30 +1966,32 @@ platf::pix_fmt_e map_pix_fmt(AVPixelFormat fmt) {
|
||||
return platf::pix_fmt_e::unknown;
|
||||
}
|
||||
|
||||
color_t make_color_matrix(float Cr, float Cb, float U_max, float V_max, float add_Y, float add_UV, const float2 &range_Y, const float2 &range_UV) {
|
||||
color_t make_color_matrix(float Cr, float Cb, const float2 &range_Y, const float2 &range_UV) {
|
||||
float Cg = 1.0f - Cr - Cb;
|
||||
|
||||
float Cr_i = 1.0f - Cr;
|
||||
float Cb_i = 1.0f - Cb;
|
||||
|
||||
float shift_y = range_Y[0] / 256.0f;
|
||||
float shift_uv = range_UV[0] / 256.0f;
|
||||
float shift_y = range_Y[0] / 255.0f;
|
||||
float shift_uv = range_UV[0] / 255.0f;
|
||||
|
||||
float scale_y = (range_Y[1] - range_Y[0]) / 256.0f;
|
||||
float scale_uv = (range_UV[1] - range_UV[0]) / 256.0f;
|
||||
float scale_y = (range_Y[1] - range_Y[0]) / 255.0f;
|
||||
float scale_uv = (range_UV[1] - range_UV[0]) / 255.0f;
|
||||
return {
|
||||
{ Cr, Cg, Cb, add_Y },
|
||||
{ -(Cr * U_max / Cb_i), -(Cg * U_max / Cb_i), U_max, add_UV },
|
||||
{ V_max, -(Cg * V_max / Cr_i), -(Cb * V_max / Cr_i), add_UV },
|
||||
{ Cr, Cg, Cb, 0.0f },
|
||||
{ -(Cr * 0.5f / Cb_i), -(Cg * 0.5f / Cb_i), 0.5f, 0.5f },
|
||||
{ 0.5f, -(Cg * 0.5f / Cr_i), -(Cb * 0.5f / Cr_i), 0.5f },
|
||||
{ scale_y, shift_y },
|
||||
{ scale_uv, shift_uv },
|
||||
};
|
||||
}
|
||||
|
||||
color_t colors[] {
|
||||
make_color_matrix(0.299f, 0.114f, 0.436f, 0.615f, 0.0625, 0.5f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT601 MPEG
|
||||
make_color_matrix(0.299f, 0.114f, 0.5f, 0.5f, 0.0f, 0.5f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT601 JPEG
|
||||
make_color_matrix(0.2126f, 0.0722f, 0.436f, 0.615f, 0.0625, 0.5f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT701 MPEG
|
||||
make_color_matrix(0.2126f, 0.0722f, 0.5f, 0.5f, 0.0f, 0.5f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT701 JPEG
|
||||
make_color_matrix(0.299f, 0.114f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT601 MPEG
|
||||
make_color_matrix(0.299f, 0.114f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT601 JPEG
|
||||
make_color_matrix(0.2126f, 0.0722f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT709 MPEG
|
||||
make_color_matrix(0.2126f, 0.0722f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT709 JPEG
|
||||
make_color_matrix(0.2627f, 0.0593f, { 16.0f, 235.0f }, { 16.0f, 240.0f }), // BT2020 MPEG
|
||||
make_color_matrix(0.2627f, 0.0593f, { 0.0f, 255.0f }, { 0.0f, 255.0f }), // BT2020 JPEG
|
||||
};
|
||||
} // namespace video
|
||||
|
||||
@@ -72,7 +72,7 @@ struct __attribute__((__aligned__(16))) color_t {
|
||||
float2 range_uv;
|
||||
};
|
||||
|
||||
extern color_t colors[4];
|
||||
extern color_t colors[6];
|
||||
|
||||
void capture(
|
||||
safe::mail_t mail,
|
||||
|
||||
BIN
src_assets/common/assets/desktop-alt.png
Normal file
BIN
src_assets/common/assets/desktop-alt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
src_assets/common/assets/desktop.png
Normal file
BIN
src_assets/common/assets/desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 50 KiB |
@@ -49,6 +49,20 @@
|
||||
The minimum log level printed to standard out
|
||||
</div>
|
||||
</div>
|
||||
<!--Log Path-->
|
||||
<div class="mb-3">
|
||||
<label for="log_path" class="form-label">Logfile Path</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="log_path"
|
||||
placeholder="sunshine.log"
|
||||
v-model="config.log_path"
|
||||
/>
|
||||
<div class="form-text">
|
||||
The file where the current logs of Sunshine are stored.
|
||||
</div>
|
||||
</div>
|
||||
<!--Origin Web UI Allowed-->
|
||||
<div class="mb-3">
|
||||
<label for="origin_web_ui_allowed" class="form-label"
|
||||
@@ -447,7 +461,7 @@
|
||||
</div>
|
||||
<!-- Quantization Parameter -->
|
||||
<div class="mb-3">
|
||||
<label for="qp" class="form-label">Quantitization Parameter</label>
|
||||
<label for="qp" class="form-label">Quantization Parameter</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
@@ -456,7 +470,7 @@
|
||||
v-model="config.qp"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Quantitization Parameter<br />
|
||||
Quantization Parameter<br />
|
||||
Some devices may not support Constant Bit Rate.<br />
|
||||
For those devices, QP is used instead.<br />
|
||||
Higher value means more compression, but less quality<br />
|
||||
@@ -465,7 +479,7 @@
|
||||
<!-- Min Threads -->
|
||||
<div class="mb-3">
|
||||
<label for="min_threads" class="form-label"
|
||||
>Minimum number of threads used by ffmpeg to encode the video.</label
|
||||
>Minimum Software Encoding Thread Count</label
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@@ -476,7 +490,6 @@
|
||||
v-model="config.min_threads"
|
||||
/>
|
||||
<div class="form-text">
|
||||
Minimum number of threads used by ffmpeg to encode the video.<br />
|
||||
Increasing the value slightly reduces encoding efficiency, but the tradeoff is usually<br />
|
||||
worth it to gain the use of more CPU cores for encoding. The ideal value is the lowest<br />
|
||||
value that can reliably encode at your desired streaming settings on your hardware.
|
||||
@@ -509,9 +522,10 @@
|
||||
<label for="encoder" class="form-label">Force a Specific Encoder</label>
|
||||
<select id="encoder" class="form-select" v-model="config.encoder">
|
||||
<option value>Autodetect</option>
|
||||
<option value="nvenc">nVidia NVENC</option>
|
||||
<option value="amdvce">AMD AMF/VCE</option>
|
||||
<option value="vaapi">VA-API</option>
|
||||
<option value="nvenc" v-if="platform === 'windows' || platform === 'linux'">NVIDIA NVENC</option>
|
||||
<option value="amdvce" v-if="platform === 'windows'">AMD AMF/VCE</option>
|
||||
<option value="vaapi" v-if="platform === 'linux'">VA-API</option>
|
||||
<option value="videotoolbox" v-if="platform === 'macos'">VideoToolbox</option>
|
||||
<option value="software">Software</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
@@ -644,40 +658,40 @@
|
||||
<div v-if="currentTab === 'nv'" class="config-page">
|
||||
<!--NVENC SETTINGS-->
|
||||
<div class="mb-3">
|
||||
<label for="nv_preset" class="form-label">NVEnc Preset</label>
|
||||
<label for="nv_preset" class="form-label">NVENC Preset</label>
|
||||
<select id="nv_preset" class="form-select" v-model="config.nv_preset">
|
||||
<option value="default">Default</option>
|
||||
<option value="hp">High Performance</option>
|
||||
<option value="hq">High Quality</option>
|
||||
<option value="slow">Slow - hq 2 passes</option>
|
||||
<option value="medium">medium -- hq 1 pass</option>
|
||||
<option value="fast">fast -- hp 1 pass</option>
|
||||
<option value="bd">bd</option>
|
||||
<option value="p1">p1 -- fastest (lowest quality)</option>
|
||||
<option value="p2">p2 -- faster (lower quality)</option>
|
||||
<option value="p3">p3 -- fast (low quality)</option>
|
||||
<option value="p4">p4 -- medium (default)</option>
|
||||
<option value="p5">p5 -- slow (good quality)</option>
|
||||
<option value="p6">p6 -- slower (better quality)</option>
|
||||
<option value="p7">p7 -- slowest (best quality)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_tune" class="form-label">NVENC Tune</label>
|
||||
<select id="nv_tune" class="form-select" v-model="config.nv_tune">
|
||||
<option value="hq">hq -- high quality</option>
|
||||
<option value="ll">ll -- low latency</option>
|
||||
<option value="llhq">llhq</option>
|
||||
<option value="llhp">llhp</option>
|
||||
<option value="lossless">lossless</option>
|
||||
<option value="losslesshp">losslesshp</option>
|
||||
<option value="ull">ull -- ultra low latency (default)</option>
|
||||
<option value="lossless">lossless -- lossless</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_rc" class="form-label">NVEnc Rate Control</label>
|
||||
<label for="nv_rc" class="form-label">NVENC Rate Control</label>
|
||||
<select id="nv_rc" class="form-select" v-model="config.nv_rc">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="constqp">constqp -- constant qp mode</option>
|
||||
<option value="vbr">vbr -- variable bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
<option value="cbr_hq">cbr_hq -- cbr high quality</option>
|
||||
<option value="cbr_ld_hq">cbr_ld_hq -- cbr low delay high quality</option>
|
||||
<option value="vbr_hq">vbr_hq -- vbr high quality</option>
|
||||
<option value="cbr">cbr -- constant bitrate (default)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="nv_coder" class="form-label">NVEnc Coder</label>
|
||||
<label for="nv_coder" class="form-label">NVENC Coder (H264)</label>
|
||||
<select id="nv_coder" class="form-select" v-model="config.nv_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
<option value="auto">auto -- let ffmpeg decide (default)</option>
|
||||
<option value="cabac">cabac -- context adaptive binary arithmetic coding - higher quality</option>
|
||||
<option value="cavlc">cavlc -- context adaptive variable-length coding - faster decode</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -685,36 +699,35 @@
|
||||
<div v-if="currentTab === 'amd'" class="config-page">
|
||||
<!--Presets-->
|
||||
<div class="mb-3">
|
||||
<label for="amd_quality" class="form-label">AMD AMF Quality</label>
|
||||
<label for="amd_quality" class="form-label">AMF Quality</label>
|
||||
<select
|
||||
id="amd_quality"
|
||||
class="form-select"
|
||||
v-model="config.amd_quality"
|
||||
>
|
||||
<option value="default">Default</option>
|
||||
<option value="speed">Speed</option>
|
||||
<option value="balanced">Balanced</option>
|
||||
v-model="config.amd_quality">
|
||||
<option value="speed">speed -- prefer speed</option>
|
||||
<option value="balanced">balanced -- balanced (default)</option>
|
||||
<option value="quality">quality -- prefer quality</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_rc" class="form-label">AMD AMF Rate Control</label>
|
||||
<label for="amd_rc" class="form-label">AMF Rate Control</label>
|
||||
<select id="amd_rc" class="form-select" v-model="config.amd_rc">
|
||||
<option value="auto">auto -- let ffmpeg decide rate control</option>
|
||||
<option value="constqp">constqp -- constant QP mode</option>
|
||||
<option value="vbr_latency">vbr_latency -- Latency Constrained Variable Bitrate</option>
|
||||
<option value="vbr_peak">vbr_peak -- Peak Contrained Variable Bitrate</option>
|
||||
<option value="cqp">cqp -- constant qp mode</option>
|
||||
<option value="vbr_latency">vbr_latency -- latency constrained variable bitrate (default)</option>
|
||||
<option value="vbr_peak">vbr_peak -- peak contrained variable bitrate</option>
|
||||
<option value="cbr">cbr -- constant bitrate</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="amd_coder" class="form-label">AMD AMF Coder</label>
|
||||
<label for="amd_coder" class="form-label">AMF Coder (H264)</label>
|
||||
<select id="amd_coder" class="form-select" v-model="config.amd_coder">
|
||||
<option value="auto">auto</option>
|
||||
<option value="cabac">cabac</option>
|
||||
<option value="cavlc">cavlc</option>
|
||||
<option value="auto">auto -- let ffmpeg decide (default)</option>
|
||||
<option value="cabac">cabac -- context adaptive variable-length coding - higher quality</option>
|
||||
<option value="cavlc">cavlc -- context adaptive binary arithmetic coding - faster decode</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!--VA-API Encoder Settings-->
|
||||
<div v-if="currentTab === 'va-api'" class="config-page">
|
||||
<input
|
||||
class="form-control"
|
||||
@@ -752,11 +765,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-success my-4" v-if="success">
|
||||
<b>Success!</b> Restart Sunshine to apply changes
|
||||
<div class="alert alert-success my-4" v-if="saved && restart_supported">
|
||||
<b>Success!</b> Click 'Apply' to restart Sunshine and apply changes. This will terminate any running sessions.
|
||||
</div>
|
||||
<div class="alert alert-success my-4" v-if="saved && !restart_supported">
|
||||
<b>Success!</b> Restart Sunshine to apply changes.
|
||||
</div>
|
||||
<div class="alert alert-success my-4" v-if="restarted">
|
||||
<b>Success!</b> Sunshine is restarting to apply changes.
|
||||
</div>
|
||||
<div class="mb-3 buttons">
|
||||
<button class="btn btn-primary" @click="save">Save</button>
|
||||
<button class="btn btn-success" @click="apply" v-if="saved && restart_supported && !restarted">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -766,7 +786,9 @@
|
||||
data() {
|
||||
return {
|
||||
platform: "",
|
||||
success: false,
|
||||
restart_supported: false,
|
||||
saved: false,
|
||||
restarted: false,
|
||||
config: null,
|
||||
fps: [],
|
||||
resolutions: [],
|
||||
@@ -794,25 +816,25 @@
|
||||
id: "advanced",
|
||||
name: "Advanced",
|
||||
},
|
||||
{
|
||||
id: "sw",
|
||||
name: "Software Encoder",
|
||||
},
|
||||
{
|
||||
id: "nv",
|
||||
name: "NVENC Encoder",
|
||||
name: "NVIDIA NVENC Encoder",
|
||||
},
|
||||
{
|
||||
id: "amd",
|
||||
name: "AMF Encoder",
|
||||
name: "AMD AMF Encoder",
|
||||
},
|
||||
{
|
||||
id: "va-api",
|
||||
name: "VA-API encoder",
|
||||
name: "VA-API Encoder",
|
||||
},
|
||||
{
|
||||
id: "vt",
|
||||
name: "VideoToolbox encoder",
|
||||
name: "VideoToolbox Encoder",
|
||||
},
|
||||
{
|
||||
id: "sw",
|
||||
name: "Software Encoder",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -823,21 +845,28 @@
|
||||
.then((r) => {
|
||||
this.config = r;
|
||||
this.platform = this.config.platform;
|
||||
this.restart_supported = (this.config.restart_supported === "true");
|
||||
|
||||
var app = document.getElementById("app");
|
||||
if (this.platform == "windows") {
|
||||
this.tabs = this.tabs.filter((el) => {
|
||||
return el.id !== "va-api";
|
||||
return el.id !== "va-api" && el.id !== "vt";
|
||||
});
|
||||
}
|
||||
if (this.platform == "linux") {
|
||||
this.tabs = this.tabs.filter((el) => {
|
||||
return el.id !== "amd";
|
||||
return el.id !== "amd" && el.id !== "vt";
|
||||
});
|
||||
}
|
||||
if (this.platform == "macos") {
|
||||
this.tabs = this.tabs.filter((el) => {
|
||||
return el.id !== "amd" && el.id !== "nv" && el.id !== "va-api";
|
||||
});
|
||||
}
|
||||
|
||||
delete this.config.status;
|
||||
delete this.config.platform;
|
||||
delete this.config.restart_supported;
|
||||
//Populate default values if not present in config
|
||||
this.config.key_rightalt_to_key_win =
|
||||
this.config.key_rightalt_to_key_win || "disabled";
|
||||
@@ -848,15 +877,16 @@
|
||||
this.config.origin_pin_allowed =
|
||||
this.config.origin_pin_allowed || "pc";
|
||||
this.config.origin_web_ui_allowed =
|
||||
this.config.origin_web_manager_allowed || "lan";
|
||||
this.config.origin_web_ui_allowed || "lan";
|
||||
this.config.hevc_mode = this.config.hevc_mode || 0;
|
||||
this.config.encoder = this.config.encoder || "";
|
||||
this.config.nv_preset = this.config.nv_preset || "default";
|
||||
this.config.nv_rc = this.config.nv_rc || "auto";
|
||||
this.config.nv_preset = this.config.nv_preset || "p4";
|
||||
this.config.nv_tune = this.config.nv_tune || "ull";
|
||||
this.config.nv_coder = this.config.nv_coder || "auto";
|
||||
this.config.nv_rc = this.config.nv_rc || "cbr";
|
||||
this.config.amd_coder = this.config.amd_coder || "auto"
|
||||
this.config.amd_quality = this.config.amd_quality || "default";
|
||||
this.config.amd_rc = this.config.amd_rc || "auto";
|
||||
this.config.amd_quality = this.config.amd_quality || "balanced";
|
||||
this.config.amd_rc = this.config.amd_rc || "vbr_latency";
|
||||
this.config.vt_coder = this.config.vt_coder || "auto";
|
||||
this.config.vt_software = this.config.vt_software || "auto";
|
||||
this.config.vt_realtime = this.config.vt_realtime || "enabled";
|
||||
@@ -876,8 +906,7 @@
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.success = false;
|
||||
serialize() {
|
||||
let nl = this.config === "windows" ? "\r\n" : "\n";
|
||||
this.config.resolutions =
|
||||
"[" +
|
||||
@@ -887,12 +916,31 @@
|
||||
nl +
|
||||
"]";
|
||||
this.config.fps = JSON.stringify(this.fps);
|
||||
|
||||
},
|
||||
save() {
|
||||
this.saved = this.restarted = false;
|
||||
this.serialize();
|
||||
fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.config),
|
||||
}).then((r) => {
|
||||
if (r.status == 200) this.success = true;
|
||||
if (r.status == 200) this.saved = true;
|
||||
});
|
||||
},
|
||||
apply() {
|
||||
this.saved = this.restarted = false;
|
||||
this.serialize();
|
||||
fetch("/api/config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(this.config),
|
||||
}).then((r) => {
|
||||
if (r.status == 200) {
|
||||
fetch("/api/restart", {
|
||||
method: "POST",
|
||||
}).then((r) => {
|
||||
if (r.status == 200) this.restarted = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div id="content" class="container">
|
||||
<h1 class="my-4">Hello, Sunshine!</h1>
|
||||
<p>Sunshine is a Gamestream host for Moonlight</p>
|
||||
<p>Sunshine is a self-hosted game stream host for Moonlight.</p>
|
||||
<!--Resources-->
|
||||
<div class="card p-2 my-4">
|
||||
<div class="card-body">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "6.2.0",
|
||||
"bootstrap": "5.0.0",
|
||||
"vue": "2.6.12"
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,43 @@
|
||||
<br />
|
||||
<p>
|
||||
If Moonlight complains about an app currently running, force closing the
|
||||
app should fix the issue
|
||||
app should fix the issue.
|
||||
</p>
|
||||
<div class="alert alert-success" v-if="closeAppStatus === true">
|
||||
Application Closed Successfuly!
|
||||
Application Closed Successfully!
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="closeAppStatus === false">
|
||||
Error while closing Appplication
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
:disabled="closeAppPressed"
|
||||
@click="closeApp"
|
||||
>
|
||||
<button class="btn btn-warning" :disabled="closeAppPressed" @click="closeApp">
|
||||
Force Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Restart Sunshine-->
|
||||
<div class="card p-2 my-4" v-if="restartSupported">
|
||||
<div class="card-body">
|
||||
<h2>Restart Sunshine</h2>
|
||||
<br />
|
||||
<p>
|
||||
If Sunshine isn't working properly, you can try restarting it.
|
||||
This will terminate any running sessions.
|
||||
</p>
|
||||
<div class="alert alert-success" v-if="restartStatus === true">
|
||||
Sunshine is restarting
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="restartStatus === false">
|
||||
Error restarting Sunshine
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-warning" :disabled="restartPressed" @click="restart">
|
||||
Restart Sunshine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Unpair all Clients-->
|
||||
<div class="card p-2 my-4">
|
||||
<div class="card-body">
|
||||
@@ -39,16 +57,28 @@
|
||||
Error while unpairing
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
:disabled="unpairAllPressed"
|
||||
@click="unpairAll"
|
||||
>
|
||||
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
|
||||
Unpair All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Logs-->
|
||||
<div class="card p-2 my-4">
|
||||
<div class="card-body">
|
||||
<h2>Logs</h2>
|
||||
<br />
|
||||
<div class="d-flex justify-content-between align-items-baseline py-2">
|
||||
<p>See the logs uploaded by Sunshine</p>
|
||||
<input type="text" class="form-control" v-model="logFilter" placeholder="Find..." style="width: 300px">
|
||||
</div>
|
||||
<div>
|
||||
<div class="troubleshooting-logs">
|
||||
<button class="copy-icon"><i class="fas fa-copy " @click="copyLogs"></i></button>{{actualLogs}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -60,9 +90,44 @@
|
||||
closeAppStatus: null,
|
||||
unpairAllPressed: false,
|
||||
unpairAllStatus: null,
|
||||
restartSupported: false,
|
||||
restartPressed: false,
|
||||
restartStatus: null,
|
||||
logs: 'Loading...',
|
||||
logFilter: null,
|
||||
logInterval: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
actualLogs(){
|
||||
if(!this.logFilter)return this.logs;
|
||||
let lines = this.logs.split("\n");
|
||||
lines = lines.filter(x => x.indexOf(this.logFilter) != -1);
|
||||
return lines.join("\n");
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.logInterval = setInterval(() => {
|
||||
this.refreshLogs();
|
||||
}, 5000);
|
||||
this.refreshLogs();
|
||||
fetch("/api/config")
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
this.restartSupported = (r.restart_supported === "true");
|
||||
});
|
||||
},
|
||||
beforeDestroy(){
|
||||
clearInterval(this.logInterval);
|
||||
},
|
||||
methods: {
|
||||
refreshLogs() {
|
||||
fetch("/api/logs",)
|
||||
.then((r) => r.text())
|
||||
.then((r) => {
|
||||
this.logs = r;
|
||||
});
|
||||
},
|
||||
closeApp() {
|
||||
this.closeAppPressed = true;
|
||||
fetch("/api/apps/close", { method: "POST" })
|
||||
@@ -87,6 +152,55 @@
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
copyLogs(){
|
||||
navigator.clipboard.writeText(this.actualLogs);
|
||||
},
|
||||
restart() {
|
||||
this.restartPressed = true;
|
||||
fetch("/api/restart", {
|
||||
method: "POST",
|
||||
}).then((r) => {
|
||||
this.restartPressed = false;
|
||||
|
||||
// We won't get a response in the success case
|
||||
this.restartStatus = r.status.toString() !== "false";
|
||||
|
||||
setTimeout(() => {
|
||||
this.restartStatus = null;
|
||||
}, 5000);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.troubleshooting-logs {
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
min-height: 500px;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
color: rgba(0,0,0,1);
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.copy-icon:hover {
|
||||
color: rgba(0,0,0,0.75);
|
||||
}
|
||||
.copy-icon:active {
|
||||
color: rgba(0,0,0,1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
"PATH": "$(PATH):$(HOME)/.local/bin"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"name": "Desktop",
|
||||
"image-path": "desktop.png"
|
||||
},
|
||||
{
|
||||
"name": "Low Res Desktop",
|
||||
"image-path": "desktop.png",
|
||||
"prep-cmd": [
|
||||
{
|
||||
"do": "xrandr --output HDMI-1 --mode 1920x1080",
|
||||
@@ -14,7 +19,6 @@
|
||||
},
|
||||
{
|
||||
"name": "Steam BigPicture",
|
||||
"output": "steam.txt",
|
||||
"detached": [
|
||||
"setsid steam steam://open/bigpicture"
|
||||
],
|
||||
|
||||
@@ -31,5 +31,5 @@ void main() {
|
||||
u = u * range_uv.x + range_uv.y;
|
||||
v = v * range_uv.x + range_uv.y;
|
||||
|
||||
color = vec2(u, v * 224.0f / 256.0f + 0.0625);
|
||||
color = vec2(u, v);
|
||||
}
|
||||
@@ -2,5 +2,17 @@
|
||||
"env": {
|
||||
"PATH": "$(PATH):$(HOME)/.local/bin"
|
||||
},
|
||||
"apps": []
|
||||
"apps": [
|
||||
{
|
||||
"name": "Desktop",
|
||||
"image-path": "desktop.png"
|
||||
},
|
||||
{
|
||||
"name": "Steam BigPicture",
|
||||
"detached": [
|
||||
"open steam://open/bigpicture"
|
||||
],
|
||||
"image-path": "steam.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"env": {
|
||||
"PATH": "$(PATH);C:\\Program Files (x86)\\Steam"
|
||||
"PATH": "$(PATH);$(ProgramFiles(x86))\\Steam"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"name": "Desktop",
|
||||
"image-path": "desktop.png"
|
||||
},
|
||||
{
|
||||
"name": "Steam BigPicture",
|
||||
"output": "steam.txt",
|
||||
"detached": [
|
||||
"steam steam://open/bigpicture"
|
||||
],
|
||||
|
||||
@@ -29,5 +29,5 @@ float2 main_ps(FragTexWide input) : SV_Target
|
||||
u = u * range_uv.x + range_uv.y;
|
||||
v = v * range_uv.x + range_uv.y;
|
||||
|
||||
return float2(u, v * 224.0f/256.0f + 0.0625);
|
||||
return float2(u, v);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ struct PS_INPUT
|
||||
float main_ps(PS_INPUT frag_in) : SV_Target
|
||||
{
|
||||
float3 rgb = image.Sample(def_sampler, frag_in.tex, 0).rgb;
|
||||
float y = dot(color_vec_y.xyz, rgb);
|
||||
float y = dot(color_vec_y.xyz, rgb) + color_vec_y.w;
|
||||
|
||||
return y * range_y.x + range_y.y;
|
||||
}
|
||||
46
src_assets/windows/misc/migration/migrate-config.bat
Normal file
46
src_assets/windows/misc/migration/migrate-config.bat
Normal file
@@ -0,0 +1,46 @@
|
||||
@echo off
|
||||
|
||||
rem Get sunshine root directory
|
||||
for %%I in ("%~dp0\..") do set "OLD_DIR=%%~fI"
|
||||
|
||||
rem Create the config directory if it didn't already exist
|
||||
set "NEW_DIR=%OLD_DIR%\config"
|
||||
if not exist "%NEW_DIR%\" mkdir "%NEW_DIR%"
|
||||
|
||||
rem Migrate all files that aren't already present in the config dir
|
||||
if exist "%OLD_DIR%\apps.json" (
|
||||
if not exist "%NEW_DIR%\apps.json" (
|
||||
move "%OLD_DIR%\apps.json" "%NEW_DIR%\apps.json"
|
||||
)
|
||||
)
|
||||
if exist "%OLD_DIR%\sunshine.conf" (
|
||||
if not exist "%NEW_DIR%\sunshine.conf" (
|
||||
move "%OLD_DIR%\sunshine.conf" "%NEW_DIR%\sunshine.conf"
|
||||
)
|
||||
)
|
||||
if exist "%OLD_DIR%\sunshine_state.json" (
|
||||
if not exist "%NEW_DIR%\sunshine_state.json" (
|
||||
move "%OLD_DIR%\sunshine_state.json" "%NEW_DIR%\sunshine_state.json"
|
||||
)
|
||||
)
|
||||
|
||||
rem Migrate the credentials directory
|
||||
if exist "%OLD_DIR%\credentials\" (
|
||||
if not exist "%NEW_DIR%\credentials\" (
|
||||
move "%OLD_DIR%\credentials" "%NEW_DIR%\"
|
||||
)
|
||||
)
|
||||
|
||||
rem Migrate the covers directory
|
||||
if exist "%OLD_DIR%\covers\" (
|
||||
if not exist "%NEW_DIR%\covers\" (
|
||||
move "%OLD_DIR%\covers" "%NEW_DIR%\"
|
||||
|
||||
rem Fix apps.json image path values that point at the old covers directory
|
||||
powershell -c "(Get-Content '%NEW_DIR%\apps.json').replace('.\/covers\/', '.\/config\/covers\/') | Set-Content '%NEW_DIR%\apps.json'"
|
||||
)
|
||||
)
|
||||
|
||||
rem Remove log files
|
||||
del "%OLD_DIR%\*.txt"
|
||||
del "%OLD_DIR%\*.log"
|
||||
@@ -23,5 +23,8 @@ if %ERRORLEVEL%==0 (
|
||||
rem Run the sc command to create/reconfigure the service
|
||||
sc %SC_CMD% %SERVICE_NAME% binPath= %SERVICE_BIN% start= %SERVICE_START_TYPE%
|
||||
|
||||
rem Set the description of the service
|
||||
sc description %SERVICE_NAME% "Sunshine is a self-hosted game stream host for Moonlight."
|
||||
|
||||
rem Start the new service
|
||||
net start %SERVICE_NAME%
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user