Merge pull request #567 from LizardByte/nightly

v0.17.0
This commit is contained in:
ReenigneArcher
2023-01-08 21:03:16 -05:00
committed by GitHub
160 changed files with 5303 additions and 23943 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,7 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "6.2.1",
"bootstrap": "5.2.3",
"vue": "2.6.12"
}
}

View File

@@ -25,7 +25,7 @@ prepare() {
}
build() {
pushd "$pkgname/src_assets/common/assets/web"
pushd "$pkgname"
npm install
popd

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -84,4 +84,4 @@ public:
std::unique_ptr<audio_control_t> audio_control() {
return std::make_unique<macos_audio_control_t>();
}
}
} // namespace platf

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "6.2.0",
"bootstrap": "5.0.0",
"vue": "2.6.12"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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