Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
882b61ac63 | ||
|
|
4c68a5d8c3 | ||
|
|
452b001c99 | ||
|
|
cac639806c | ||
|
|
dbca1bb4ce |
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Follow the troubleshooting guide before reporting a bug
|
||||
|
||||
---
|
||||
**READ ME FIRST!**
|
||||
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||
|
||||
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to reproduce**
|
||||
Any special steps that are required for the bug to appear.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||
|
||||
**Affected games**
|
||||
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||
|
||||
**Other Moonlight clients**
|
||||
- Does the issue occur when using Moonlight on PC or Android?
|
||||
|
||||
**Moonlight settings (please complete the following information)**
|
||||
- Have any settings been adjusted from defaults?
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||
- Do you have any gamepads connected to your host PC directly?
|
||||
- If gamepad input is not working, does it work if you use Moonlight's on-screen controls?
|
||||
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||
|
||||
**Device details (please complete the following information)**
|
||||
- iOS/tvOS version: [e.g. iOS 14.2]
|
||||
- Device model: [e.g. iPhone 11 Pro]
|
||||
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||
|
||||
**Additional context**
|
||||
Anything else you think may be relevant to the issue or special about your specific setup.
|
||||
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 60
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: false
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- enhancement
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: >
|
||||
This issue has been automatically locked since there has not been
|
||||
any recent activity after it was closed. If you experience an issue
|
||||
that looks similar to this one, please open a new issue with details
|
||||
about your specific streaming setup.
|
||||
|
||||
# Don't lock PRs
|
||||
only: issues
|
||||
@@ -1,45 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build (${{ matrix.platform }})
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- scheme: Moonlight
|
||||
sdk: iphonesimulator
|
||||
platform: iOS
|
||||
- scheme: Moonlight TV
|
||||
sdk: appletvsimulator
|
||||
platform: tvOS
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download ${{ matrix.platform }} platform
|
||||
run: xcodebuild -downloadPlatform ${{ matrix.platform }}
|
||||
|
||||
- name: Build Debug
|
||||
run: xcodebuild -project Moonlight.xcodeproj -scheme "${{ matrix.scheme }}" -configuration Debug -sdk ${{ matrix.sdk }} clean build
|
||||
|
||||
- name: Build Release
|
||||
run: xcodebuild -project Moonlight.xcodeproj -scheme "${{ matrix.scheme }}" -configuration Release -sdk ${{ matrix.sdk }} clean build
|
||||
@@ -1,6 +1,4 @@
|
||||
**/xcuserdata/
|
||||
**/Moonlight.xcscmblueprint
|
||||
**/xcschemes/*.xcscheme
|
||||
.DS_Store
|
||||
Build
|
||||
DerivedData
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "moonlight-common/moonlight-common-c"]
|
||||
path = moonlight-common/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
[submodule "X1Kit"]
|
||||
path = X1Kit
|
||||
url = https://github.com/cgutman/X1Kit.git
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
language: objective-c
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode11.5
|
||||
env:
|
||||
- SCHEME="Moonlight"
|
||||
- TARGET="platform=iOS Simulator,OS=13.5,name=iPhone 11 Pro"
|
||||
- os: osx
|
||||
osx_image: xcode11.5
|
||||
env:
|
||||
- SCHEME="Moonlight"
|
||||
- TARGET="platform=iOS Simulator,OS=13.5,name=iPad Pro (9.7-inch)"
|
||||
- os: osx
|
||||
osx_image: xcode11.5
|
||||
env:
|
||||
- SCHEME="Moonlight TV"
|
||||
- TARGET="platform=tvOS Simulator,OS=13.4,name=Apple TV 4K"
|
||||
|
||||
script:
|
||||
- set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "$SCHEME" -destination "$TARGET" build | xcpretty
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
source config.sh
|
||||
|
||||
# Number of CPUs (for make -j)
|
||||
NCPU=`sysctl -n hw.ncpu`
|
||||
if test x$NJOB = x; then
|
||||
NJOB=$NCPU
|
||||
fi
|
||||
|
||||
PLATFORMBASE=$(xcode-select -print-path)"/Platforms"
|
||||
|
||||
SCRIPT_DIR=$( (cd -P $(dirname $0) && pwd) )
|
||||
DIST_DIR_BASE=${DIST_DIR_BASE:="$SCRIPT_DIR/dist"}
|
||||
|
||||
if [ ! -d opus ]
|
||||
then
|
||||
echo "opus source directory does not exist, run sync.sh"
|
||||
fi
|
||||
|
||||
# PATH=${SCRIPT_DIR}/gas-preprocessor/:$PATH
|
||||
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
OPUS_DIR=opus-$ARCH
|
||||
if [ ! -d $OPUS_DIR ]
|
||||
then
|
||||
echo "Directory $OPUS_DIR does not exist, run sync.sh"
|
||||
exit 1
|
||||
fi
|
||||
echo "Compiling source for $ARCH in directory $OPUS_DIR"
|
||||
echo "cd $OPUS_DIR"
|
||||
cd $OPUS_DIR
|
||||
|
||||
DIST_DIR=$DIST_DIR_BASE-$ARCH
|
||||
echo "mkdir -p $DIST_DIR"
|
||||
mkdir -p $DIST_DIR
|
||||
CFLAGS_ARCH=$ARCH
|
||||
case $ARCH in
|
||||
armv7)
|
||||
EXTRA_FLAGS="--with-pic --enable-fixed-point"
|
||||
EXTRA_CFLAGS="-mcpu=cortex-a8 -mfpu=neon -miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
|
||||
IOSSDK=iPhoneOS
|
||||
;;
|
||||
armv7s)
|
||||
EXTRA_FLAGS="--with-pic --enable-fixed-point"
|
||||
EXTRA_CFLAGS="-mcpu=cortex-a9 -mfpu=neon -miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
|
||||
IOSSDK=iPhoneOS
|
||||
;;
|
||||
aarch64)
|
||||
CFLAGS_ARCH="arm64"
|
||||
EXTRA_FLAGS="--with-pic --enable-fixed-point"
|
||||
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
|
||||
IOSSDK=iPhoneOS
|
||||
;;
|
||||
x86_64)
|
||||
EXTRA_FLAGS="--with-pic"
|
||||
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneSimulator.platform"
|
||||
IOSSDK=iPhoneSimulator
|
||||
;;
|
||||
i386)
|
||||
EXTRA_FLAGS="--with-pic"
|
||||
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneSimulator.platform"
|
||||
IOSSDK=iPhoneSimulator
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture ${ARCH}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Configuring opus for $ARCH..."
|
||||
echo "./autogen.sh"
|
||||
./autogen.sh
|
||||
|
||||
CFLAGS="-g -O2 -pipe -arch ${CFLAGS_ARCH} \
|
||||
-isysroot ${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
|
||||
-I${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk/usr/include \
|
||||
${EXTRA_CFLAGS}"
|
||||
LDFLAGS="-arch ${CFLAGS_ARCH} \
|
||||
-isysroot ${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
|
||||
-L${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk/usr/lib"
|
||||
|
||||
echo "CFLAGS=$CFLAGS"
|
||||
echo "LDFLAGS=$LDFLAGS"
|
||||
|
||||
export CFLAGS
|
||||
export LDFLAGS
|
||||
|
||||
export CXXCPP="/usr/bin/cpp"
|
||||
export CPP="$CXXCPP"
|
||||
export CXX="/usr/bin/gcc"
|
||||
export CC="/usr/bin/gcc"
|
||||
export LD="/usr/bin/ld"
|
||||
export AR="/usr/bin/ar"
|
||||
export AS="/usr/bin/ls"
|
||||
export NM="/usr/bin/nm"
|
||||
export RANLIB="/usr/bin/ranlib"
|
||||
export STRIP="/usr/bin/strip"
|
||||
echo "./configure \
|
||||
--prefix=$DIST_DIR \
|
||||
--host=${ARCH}-apple-darwin \
|
||||
--with-sysroot=${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
|
||||
--enable-static=yes \
|
||||
--enable-shared=no \
|
||||
--disable-doc \
|
||||
${EXTRA_FLAGS}"
|
||||
|
||||
./configure \
|
||||
--prefix=$DIST_DIR \
|
||||
--host=${ARCH}-apple-darwin \
|
||||
--with-sysroot=${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
|
||||
--enable-static=yes \
|
||||
--enable-shared=no \
|
||||
--disable-doc \
|
||||
${EXTRA_FLAGS}
|
||||
|
||||
echo "Installing opus for $ARCH..."
|
||||
echo "make clean"
|
||||
make clean
|
||||
echo "make -j$NJOB V=1"
|
||||
make -j$NJOB V=1
|
||||
echo "make install"
|
||||
make install
|
||||
|
||||
echo "cd $SCRIPT_DIR"
|
||||
cd $SCRIPT_DIR
|
||||
|
||||
if [ -d $DIST_DIR/bin ]
|
||||
then
|
||||
rm -rf $DIST_DIR/bin
|
||||
fi
|
||||
if [ -d $DIST_DIR/share ]
|
||||
then
|
||||
rm -rf $DIST_DIR/share
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
source config.sh
|
||||
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
|
||||
if [ -d dist-$ARCH ]
|
||||
then
|
||||
MAIN_ARCH=$ARCH
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MAIN_ARCH" ]
|
||||
then
|
||||
echo "Please compile an architecture"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
OUTPUT_DIR="dist-uarch"
|
||||
rm -rf $OUTPUT_DIR
|
||||
|
||||
mkdir -p $OUTPUT_DIR/lib $OUTPUT_DIR/include
|
||||
|
||||
for LIB in dist-$MAIN_ARCH/lib/*.a
|
||||
do
|
||||
LIB=`basename $LIB`
|
||||
LIPO_CREATE=""
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
LIPO_ARCH=$ARCH
|
||||
if [ "$ARCH" = "aarch64" ];
|
||||
then
|
||||
LIPO_ARCH="arm64"
|
||||
fi
|
||||
if [ -d dist-$ARCH ]
|
||||
then
|
||||
LIPO_CREATE="$LIPO_CREATE-arch $LIPO_ARCH dist-$ARCH/lib/$LIB "
|
||||
fi
|
||||
done
|
||||
OUTPUT="$OUTPUT_DIR/lib/$LIB"
|
||||
echo "Creating: $OUTPUT"
|
||||
xcrun -sdk iphoneos lipo -create $LIPO_CREATE -output $OUTPUT
|
||||
xcrun -sdk iphoneos lipo -info $OUTPUT
|
||||
done
|
||||
|
||||
echo "Copying headers from dist-$MAIN_ARCH..."
|
||||
cp -R dist-$MAIN_ARCH/include/* $OUTPUT_DIR/include
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
OPUS_DIR="opus"
|
||||
IOSSDK_VER=8.1
|
||||
ARCHS="armv7 armv7s aarch64 i386 x86_64"
|
||||
|
||||
remove_arch() {
|
||||
OLD_ARCHS="$ARCHS"
|
||||
NEW_ARCHS=""
|
||||
REMOVAL="$1"
|
||||
|
||||
for ARCH in $OLD_ARCHS; do
|
||||
if [ "$ARCH" != "$REMOVAL" ] ; then
|
||||
NEW_ARCHS="$NEW_ARCHS $ARCH"
|
||||
fi
|
||||
done
|
||||
|
||||
ARCHS=$NEW_ARCHS
|
||||
}
|
||||
|
||||
# armv7s is only supported in iOS 6.0+
|
||||
CHECK=`echo $IOSSDK_VER '>= 6.0' | bc -l`
|
||||
if [ "$CHECK" = "0" ] ; then
|
||||
remove_arch "armv7s"
|
||||
fi
|
||||
|
||||
# armv6 is not supported any more since iOS 6.0
|
||||
CHECK=`echo $IOSSDK_VER '< 6.0' | bc -l`
|
||||
if [ "$CHECK" = "0" ] ; then
|
||||
remove_arch "armv6"
|
||||
fi
|
||||
|
||||
echo 'Architectures to build:' $ARCHS
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
source config.sh
|
||||
|
||||
SCRIPT_DIR=$( (cd -P $(dirname $0) && pwd) )
|
||||
DIST_DIR_BASE=${DIST_DIR_BASE:="$SCRIPT_DIR/dist"}
|
||||
|
||||
cd opus
|
||||
git checkout master
|
||||
git pull origin master
|
||||
cd ..
|
||||
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
OPUS_DIR=opus-$ARCH
|
||||
echo "Syncing source for $ARCH to directory $OPUS_DIR"
|
||||
rsync opus/ $OPUS_DIR/ --exclude '.*' -a --delete
|
||||
done
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
SYSROOT_iPHONE="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk"
|
||||
SYSROOT_SIMULATOR="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
|
||||
CC_IPHONE="xcrun -sdk iphoneos clang"
|
||||
CC_SIMULATOR="xcrun -sdk iphonesimulator clang"
|
||||
function build_one {
|
||||
./configure \
|
||||
--prefix=$PREFIX \
|
||||
--disable-ffmpeg \
|
||||
--disable-ffplay \
|
||||
--disable-ffprobe \
|
||||
--disable-ffserver \
|
||||
--disable-armv5te \
|
||||
--disable-armv6 \
|
||||
--disable-doc \
|
||||
--disable-everything \
|
||||
--disable-debug \
|
||||
--enable-decoder=h264 \
|
||||
--enable-avresample \
|
||||
--enable-cross-compile \
|
||||
--sysroot=$SYSROOT \
|
||||
--target-os=darwin \
|
||||
--cc="$CC" \
|
||||
--extra-cflags="$CFLAGS" \
|
||||
--extra-ldflags="$LDFLAGS" \
|
||||
--enable-pic \
|
||||
$ADDI_FLAGS
|
||||
make clean && make -j4 && make install
|
||||
}
|
||||
|
||||
|
||||
# armv7
|
||||
function build_armv7 {
|
||||
PREFIX="armv7"
|
||||
SYSROOT=$SYSROOT_iPHONE
|
||||
CC=$CC_IPHONE
|
||||
CFLAGS="-arch armv7 -mfpu=neon -miphoneos-version-min=7.1 -fpic"
|
||||
LDFLAGS="-arch armv7 -isysroot $SYSROOT_iPHONE -miphoneos-version-min=7.1"
|
||||
ADDI_FLAGS="--arch=arm --cpu=cortex-a9"
|
||||
build_one
|
||||
}
|
||||
|
||||
# armv7s
|
||||
function build_armv7s {
|
||||
PREFIX="armv7s"
|
||||
SYSROOT=$SYSROOT_iPHONE
|
||||
CC=$CC_IPHONE
|
||||
CFLAGS="-arch armv7s -mfpu=neon -miphoneos-version-min=7.1"
|
||||
LDFLAGS="-arch armv7s -isysroot $SYSROOT_iPHONE -miphoneos-version-min=7.1"
|
||||
ADDI_FLAGS="--arch=arm --cpu=cortex-a9"
|
||||
build_one
|
||||
}
|
||||
|
||||
# i386
|
||||
function build_i386 {
|
||||
PREFIX="i386"
|
||||
SYSROOT=$SYSROOT_SIMULATOR
|
||||
CC=$CC_SIMULATOR
|
||||
CFLAGS="-arch i386"
|
||||
LDFLAGS="-arch i386 -isysroot $SYSROOT_SIMULATOR -mios-simulator-version-min=7.1"
|
||||
ADDI_FLAGS="--arch=i386 --cpu=i386 --disable-asm"
|
||||
build_one
|
||||
}
|
||||
|
||||
|
||||
# create fat library
|
||||
function build_universal {
|
||||
cd armv7/lib
|
||||
for file in *.a
|
||||
do
|
||||
cd ../..
|
||||
xcrun -sdk iphoneos lipo -output universal/lib/$file -create \
|
||||
-arch armv7 armv7/lib/$file \
|
||||
-arch armv7s armv7s/lib/$file \
|
||||
-arch i386 i386/lib/$file
|
||||
echo "Universal $file created."
|
||||
cd -
|
||||
done
|
||||
cd ../..
|
||||
}
|
||||
|
||||
echo "FFmpeg directory?"
|
||||
read SOURCE
|
||||
|
||||
cd $SOURCE
|
||||
|
||||
if [ "$1" = "clean" ]
|
||||
then
|
||||
rm -r armv7 armv7s i386 universal
|
||||
make clean
|
||||
exit 0
|
||||
fi
|
||||
mkdir armv7
|
||||
mkdir armv7s
|
||||
mkdir i386
|
||||
mkdir -p universal/lib
|
||||
|
||||
build_armv7
|
||||
build_armv7s
|
||||
build_i386
|
||||
build_universal
|
||||
echo "Ouput files in $SOURCE/armv7 $SOURCE/armv7s $SOURCE/i386 $SOURCE/universal"
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Builds libopus for iOS or tvOS.
|
||||
#
|
||||
# Copyright 2012 Mike Tigas <mike@tig.as>
|
||||
#
|
||||
# Based on work by Felix Schulze on 16.12.10.
|
||||
# Copyright 2010 Felix Schulze. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
###########################################################################
|
||||
# Choose your libopus version and your currently-installed iOS SDK version:
|
||||
#
|
||||
TARGET="AppleTV" # "iPhone"
|
||||
VERSION="1.3.1"
|
||||
SDKVERSION="16.1"
|
||||
MINVERSIONDEF="-mtvos-version-min=12.0" # "-miphoneos-version-min=12.0"
|
||||
|
||||
###########################################################################
|
||||
#
|
||||
# Don't change anything under this line!
|
||||
#
|
||||
###########################################################################
|
||||
|
||||
# by default, we won't build for debugging purposes
|
||||
if [ "${DEBUG}" == "true" ]; then
|
||||
echo "Compiling for debugging ..."
|
||||
OPT_CFLAGS="-O0 -fno-inline -g"
|
||||
OPT_LDFLAGS=""
|
||||
OPT_CONFIG_ARGS="--enable-assertions --disable-asm"
|
||||
else
|
||||
OPT_CFLAGS="-O3 -g"
|
||||
OPT_LDFLAGS=""
|
||||
OPT_CONFIG_ARGS=""
|
||||
fi
|
||||
|
||||
|
||||
# No need to change this since xcode build will only compile in the
|
||||
# necessary bits from the libraries we create
|
||||
ARCHS="x86_64 arm64"
|
||||
|
||||
DEVELOPER=`xcode-select -print-path`
|
||||
#DEVELOPER="/Applications/Xcode.app/Contents/Developer"
|
||||
|
||||
cd "`dirname \"$0\"`"
|
||||
REPOROOT=$(pwd)
|
||||
|
||||
# Where we'll end up storing things in the end
|
||||
OUTPUTDIR="${REPOROOT}/dependencies"
|
||||
mkdir -p ${OUTPUTDIR}/include
|
||||
mkdir -p ${OUTPUTDIR}/lib
|
||||
|
||||
|
||||
BUILDDIR="${REPOROOT}/build"
|
||||
|
||||
# where we will keep our sources and build from.
|
||||
SRCDIR="${BUILDDIR}/src"
|
||||
mkdir -p $SRCDIR
|
||||
# where we will store intermediary builds
|
||||
INTERDIR="${BUILDDIR}/built"
|
||||
mkdir -p $INTERDIR
|
||||
|
||||
########################################
|
||||
|
||||
cd $SRCDIR
|
||||
|
||||
# Exit the script if an error happens
|
||||
set -e
|
||||
|
||||
if [ ! -e "${SRCDIR}/opus-${VERSION}.tar.gz" ]; then
|
||||
echo "Downloading opus-${VERSION}.tar.gz"
|
||||
curl -LO http://downloads.xiph.org/releases/opus/opus-${VERSION}.tar.gz
|
||||
fi
|
||||
echo "Using opus-${VERSION}.tar.gz"
|
||||
|
||||
tar zxf opus-${VERSION}.tar.gz -C $SRCDIR
|
||||
cd "${SRCDIR}/opus-${VERSION}"
|
||||
|
||||
set +e # don't bail out of bash script if ccache doesn't exist
|
||||
CCACHE=`which ccache`
|
||||
if [ $? == "0" ]; then
|
||||
echo "Building with ccache: $CCACHE"
|
||||
CCACHE="${CCACHE} "
|
||||
else
|
||||
echo "Building without ccache"
|
||||
CCACHE=""
|
||||
fi
|
||||
set -e # back to regular "bail out on error" mode
|
||||
|
||||
export ORIGINALPATH=$PATH
|
||||
|
||||
for ARCH in ${ARCHS}
|
||||
do
|
||||
if [ "${ARCH}" == "i386" ] || [ "${ARCH}" == "x86_64" ]; then
|
||||
PLATFORM="${TARGET}Simulator"
|
||||
EXTRA_CFLAGS="-arch ${ARCH}"
|
||||
EXTRA_CONFIG="--host=x86_64-apple-darwin"
|
||||
else
|
||||
PLATFORM="${TARGET}OS"
|
||||
EXTRA_CFLAGS="-arch ${ARCH}"
|
||||
EXTRA_CONFIG="--host=arm-apple-darwin"
|
||||
fi
|
||||
|
||||
mkdir -p "${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk"
|
||||
|
||||
./configure --disable-shared --enable-static --with-pic --disable-extra-programs --disable-doc ${EXTRA_CONFIG} \
|
||||
--prefix="${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk" \
|
||||
LDFLAGS="$LDFLAGS ${OPT_LDFLAGS} -fPIE ${MINVERSIONDEF} -L${OUTPUTDIR}/lib" \
|
||||
CFLAGS="$CFLAGS ${EXTRA_CFLAGS} ${OPT_CFLAGS} -fPIE ${MINVERSIONDEF} -I${OUTPUTDIR}/include -isysroot ${DEVELOPER}/Platforms/${PLATFORM}.platform/Developer/SDKs/${PLATFORM}${SDKVERSION}.sdk" \
|
||||
|
||||
# Build the application and install it to the fake SDK intermediary dir
|
||||
# we have set up. Make sure to clean up afterward because we will re-use
|
||||
# this source tree to cross-compile other targets.
|
||||
make -j$(nproc)
|
||||
make install
|
||||
make clean
|
||||
done
|
||||
|
||||
########################################
|
||||
|
||||
echo "Build library..."
|
||||
|
||||
# These are the libs that comprise libopus.
|
||||
OUTPUT_LIBS="libopus.a"
|
||||
for OUTPUT_LIB in ${OUTPUT_LIBS}; do
|
||||
INPUT_LIBS=""
|
||||
for ARCH in ${ARCHS}; do
|
||||
if [ "${ARCH}" == "i386" ] || [ "${ARCH}" == "x86_64" ];
|
||||
then
|
||||
PLATFORM="${TARGET}Simulator"
|
||||
else
|
||||
PLATFORM="${TARGET}OS"
|
||||
fi
|
||||
INPUT_ARCH_LIB="${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk/lib/${OUTPUT_LIB}"
|
||||
if [ -e $INPUT_ARCH_LIB ]; then
|
||||
INPUT_LIBS="${INPUT_LIBS} ${INPUT_ARCH_LIB}"
|
||||
fi
|
||||
done
|
||||
# Combine the three architectures into a universal library.
|
||||
if [ -n "$INPUT_LIBS" ]; then
|
||||
lipo -create $INPUT_LIBS \
|
||||
-output "${OUTPUTDIR}/lib/${OUTPUT_LIB}"
|
||||
else
|
||||
echo "$OUTPUT_LIB does not exist, skipping (are the dependencies installed?)"
|
||||
fi
|
||||
done
|
||||
|
||||
for ARCH in ${ARCHS}; do
|
||||
if [ "${ARCH}" == "i386" ] || [ "${ARCH}" == "x86_64" ];
|
||||
then
|
||||
PLATFORM="${TARGET}Simulator"
|
||||
else
|
||||
PLATFORM="${TARGET}OS"
|
||||
fi
|
||||
cp -R ${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk/include/* ${OUTPUTDIR}/include/
|
||||
if [ $? == "0" ]; then
|
||||
# We only need to copy the headers over once. (So break out of forloop
|
||||
# once we get first success.)
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
####################
|
||||
|
||||
echo "Building done."
|
||||
echo "Cleaning up..."
|
||||
rm -fr ${INTERDIR}
|
||||
rm -fr "${SRCDIR}/opus-${VERSION}"
|
||||
echo "Done."
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Yay shell scripting! This script builds a static version of
|
||||
# OpenSSL ${OPENSSL_VERSION} for iOS 7.0 that contains code for
|
||||
# armv6, armv7, arm7s and i386.
|
||||
|
||||
#set -x
|
||||
|
||||
# Setup paths to stuff we need
|
||||
|
||||
OPENSSL_VERSION="1.0.1h"
|
||||
|
||||
DEVELOPER="/Applications/Xcode.app/Contents/Developer"
|
||||
|
||||
SDK_VERSION="7.1"
|
||||
MIN_VERSION="7.1"
|
||||
|
||||
IPHONEOS_PLATFORM="${DEVELOPER}/Platforms/iPhoneOS.platform"
|
||||
IPHONEOS_SDK="${IPHONEOS_PLATFORM}/Developer/SDKs/iPhoneOS.sdk"
|
||||
IPHONEOS_GCC="/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang"
|
||||
|
||||
IPHONESIMULATOR_PLATFORM="${DEVELOPER}/Platforms/iPhoneSimulator.platform"
|
||||
IPHONESIMULATOR_SDK="${IPHONESIMULATOR_PLATFORM}/Developer/SDKs/iPhoneSimulator.sdk"
|
||||
IPHONESIMULATOR_GCC="/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang"
|
||||
|
||||
# Make sure things actually exist
|
||||
|
||||
if [ ! -d "$IPHONEOS_PLATFORM" ]; then
|
||||
echo "Cannot find $IPHONEOS_PLATFORM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$IPHONEOS_SDK" ]; then
|
||||
echo "Cannot find $IPHONEOS_SDK"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$IPHONEOS_GCC" ]; then
|
||||
echo "Cannot find $IPHONEOS_GCC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$IPHONESIMULATOR_PLATFORM" ]; then
|
||||
echo "Cannot find $IPHONESIMULATOR_PLATFORM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$IPHONESIMULATOR_SDK" ]; then
|
||||
echo "Cannot find $IPHONESIMULATOR_SDK"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$IPHONESIMULATOR_GCC" ]; then
|
||||
echo "Cannot find $IPHONESIMULATOR_GCC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up whatever was left from our previous build
|
||||
|
||||
rm -rf include lib
|
||||
rm -rf /tmp/openssl-${OPENSSL_VERSION}-*
|
||||
rm -rf /tmp/openssl-${OPENSSL_VERSION}-*.*-log
|
||||
|
||||
build()
|
||||
{
|
||||
TARGET=$1
|
||||
ARCH=$2
|
||||
GCC=$3
|
||||
SDK=$4
|
||||
EXTRA=$5
|
||||
rm -rf "openssl-${OPENSSL_VERSION}"
|
||||
tar xfz "openssl-${OPENSSL_VERSION}.tar.gz"
|
||||
pushd .
|
||||
cd "openssl-${OPENSSL_VERSION}"
|
||||
./Configure ${TARGET} --openssldir="/tmp/openssl-${OPENSSL_VERSION}-${ARCH}" ${EXTRA} &> "/tmp/openssl-${OPENSSL_VERSION}-${ARCH}.log"
|
||||
perl -i -pe 's|static volatile sig_atomic_t intr_signal|static volatile int intr_signal|' crypto/ui/ui_openssl.c
|
||||
perl -i -pe "s|^CC= gcc|CC= ${GCC} -arch ${ARCH} -miphoneos-version-min=${MIN_VERSION}|g" Makefile
|
||||
perl -i -pe "s|^CFLAG= (.*)|CFLAG= -isysroot ${SDK} \$1|g" Makefile
|
||||
make &> "/tmp/openssl-${OPENSSL_VERSION}-${ARCH}.build-log"
|
||||
make install &> "/tmp/openssl-${OPENSSL_VERSION}-${ARCH}.install-log"
|
||||
popd
|
||||
rm -rf "openssl-${OPENSSL_VERSION}"
|
||||
}
|
||||
|
||||
mkdir openssl
|
||||
cd openssl
|
||||
if [ ! -e ${OPENSSL_VERSION}.tar.gz ]; then
|
||||
echo "Downloading ${OPENSSL_VERSION}.tar.gz"
|
||||
curl -O http://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
|
||||
else
|
||||
echo "Using ${OPENSSL_VERSION}.tar.gz"
|
||||
fi
|
||||
|
||||
|
||||
build "BSD-generic32" "armv7" "${IPHONEOS_GCC}" "${IPHONEOS_SDK}" ""
|
||||
build "BSD-generic32" "armv7s" "${IPHONEOS_GCC}" "${IPHONEOS_SDK}" ""
|
||||
build "BSD-generic64" "arm64" "${IPHONEOS_GCC}" "${IPHONEOS_SDK}" ""
|
||||
build "BSD-generic32" "i386" "${IPHONESIMULATOR_GCC}" "${IPHONESIMULATOR_SDK}" ""
|
||||
build "BSD-generic64" "x86_64" "${IPHONESIMULATOR_GCC}" "${IPHONESIMULATOR_SDK}" "-DOPENSSL_NO_ASM"
|
||||
|
||||
#
|
||||
|
||||
mkdir include
|
||||
cp -r /tmp/openssl-${OPENSSL_VERSION}-i386/include/openssl include/
|
||||
|
||||
mkdir lib
|
||||
lipo \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-armv7/lib/libcrypto.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-armv7s/lib/libcrypto.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-arm64/lib/libcrypto.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-i386/lib/libcrypto.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-x86_64/lib/libcrypto.a" \
|
||||
-create -output lib/libcrypto.a
|
||||
lipo \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-armv7/lib/libssl.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-armv7s/lib/libssl.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-arm64/lib/libssl.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-i386/lib/libssl.a" \
|
||||
"/tmp/openssl-${OPENSSL_VERSION}-x86_64/lib/libssl.a" \
|
||||
-create -output lib/libssl.a
|
||||
|
||||
rm -rf "/tmp/openssl-${OPENSSL_VERSION}-*"
|
||||
rm -rf "/tmp/openssl-${OPENSSL_VERSION}-*.*-log"
|
||||
|
||||
@@ -672,11 +672,3 @@ may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
||||
App Store Distribution Exception
|
||||
|
||||
As additional permission under section 7, you are allowed to distribute the
|
||||
software through an app store, even if that store has restrictive terms and
|
||||
conditions that are incompatible with the GPL, provided that the source is
|
||||
also available under the GPL with or without this permission through a channel
|
||||
without those restrictive terms and conditions.
|
||||
|
||||
@@ -24,9 +24,11 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
#if !TARGET_OS_TV
|
||||
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
|
||||
if (shortcut != nil) {
|
||||
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
|
||||
if (@available(iOS 9.0, *)) {
|
||||
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
|
||||
if (shortcut != nil) {
|
||||
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return YES;
|
||||
|
||||
@@ -96,7 +96,6 @@ static NSData* p12 = nil;
|
||||
|
||||
bio = BIO_new(BIO_s_mem());
|
||||
i2d_X509_bio(bio, x509);
|
||||
X509_free(x509);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
@@ -223,7 +222,6 @@ static NSData* p12 = nil;
|
||||
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
||||
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
BIO_free(bio);
|
||||
|
||||
if (!x509) {
|
||||
Log(LOG_E, @"Unable to parse certificate in memory!");
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <OpenSSL/provider.h>
|
||||
#include <OpenSSL/rsa.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <OpenSSL/rand.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
static const int NUM_BITS = 2048;
|
||||
static const int SERIAL = 0;
|
||||
@@ -63,30 +61,22 @@ void mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
}
|
||||
|
||||
struct CertKeyPair generateCertKeyPair(void) {
|
||||
BIO *bio_err;
|
||||
X509 *x509 = NULL;
|
||||
EVP_PKEY *pkey = NULL;
|
||||
PKCS12 *p12 = NULL;
|
||||
|
||||
bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
|
||||
|
||||
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
|
||||
|
||||
char* pass = "limelight";
|
||||
p12 = PKCS12_create(pass,
|
||||
"GameStream",
|
||||
pkey,
|
||||
x509,
|
||||
NULL,
|
||||
NID_pbe_WithSHA1And3_Key_TripleDES_CBC,
|
||||
-1, // disable certificate encryption
|
||||
2048,
|
||||
-1, // disable the automatic MAC
|
||||
0);
|
||||
// MAC it ourselves with SHA1 since iOS refuses to load anything else.
|
||||
PKCS12_set_mac(p12, pass, -1, NULL, 0, 1, EVP_sha1());
|
||||
|
||||
|
||||
p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
|
||||
if (p12 == NULL) {
|
||||
printf("Error generating a valid PKCS12 certificate.\n");
|
||||
}
|
||||
|
||||
BIO_free(bio_err);
|
||||
|
||||
return (CertKeyPair){x509, pkey, p12};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,18 +17,13 @@
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
audioConfig:(NSInteger)audioConfig
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
useFramePacing:(BOOL)useFramePacing
|
||||
useHevc:(BOOL)useHevc
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
statsOverlay:(BOOL)statsOverlay;
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode;
|
||||
|
||||
- (NSArray*) getHosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
|
||||
// Main Thread Checker happy.
|
||||
if ([NSThread isMainThread]) {
|
||||
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
||||
_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
|
||||
|
||||
}
|
||||
else {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
self->_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
||||
self->_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,18 +57,13 @@
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
audioConfig:(NSInteger)audioConfig
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
swapABXYButtons:(BOOL)swapABXYButtons
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
preferredCodec:(uint32_t)preferredCodec
|
||||
useFramePacing:(BOOL)useFramePacing
|
||||
useHevc:(BOOL)useHevc
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
statsOverlay:(BOOL)statsOverlay {
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode {
|
||||
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
Settings* settingsToSave = [self retrieveSettings];
|
||||
@@ -75,18 +71,13 @@
|
||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||
settingsToSave.width = [NSNumber numberWithInteger:width];
|
||||
settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig];
|
||||
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
|
||||
settingsToSave.optimizeGames = optimizeGames;
|
||||
settingsToSave.multiController = multiController;
|
||||
settingsToSave.swapABXYButtons = swapABXYButtons;
|
||||
settingsToSave.playAudioOnPC = audioOnPC;
|
||||
settingsToSave.preferredCodec = preferredCodec;
|
||||
settingsToSave.useFramePacing = useFramePacing;
|
||||
settingsToSave.useHevc = useHevc;
|
||||
settingsToSave.enableHdr = enableHdr;
|
||||
settingsToSave.btMouseSupport = btMouseSupport;
|
||||
settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||
settingsToSave.statsOverlay = statsOverlay;
|
||||
|
||||
[self saveData];
|
||||
}];
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
@property (nullable, nonatomic, retain) NSString *name;
|
||||
@property (nullable, nonatomic, retain) NSString *installPath;
|
||||
@property (nonatomic) BOOL hdrSupported;
|
||||
@property (nonatomic) BOOL hidden;
|
||||
@property (nullable, nonatomic, retain) TemporaryHost *host;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
self.id = app.id;
|
||||
self.name = app.name;
|
||||
self.hdrSupported = app.hdrSupported;
|
||||
self.hidden = app.hidden;
|
||||
self.host = tempHost;
|
||||
|
||||
return self;
|
||||
@@ -26,7 +25,6 @@
|
||||
parent.id = self.id;
|
||||
parent.name = self.name;
|
||||
parent.hdrSupported = self.hdrSupported;
|
||||
parent.hidden = self.hidden;
|
||||
parent.host = host;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
@property (atomic) PairState pairState;
|
||||
@property (atomic, nullable, retain) NSString * activeAddress;
|
||||
@property (atomic, nullable, retain) NSString * currentGame;
|
||||
@property (atomic) unsigned short httpsPort;
|
||||
@property (atomic) BOOL isNvidiaServerSoftware;
|
||||
|
||||
@property (atomic, nullable, retain) NSData *serverCert;
|
||||
@property (atomic, nullable, retain) NSString *address;
|
||||
|
||||
@@ -34,11 +34,6 @@
|
||||
self.serverCodecModeSupport = host.serverCodecModeSupport;
|
||||
self.serverCert = host.serverCert;
|
||||
|
||||
// Older clients stored a non-URL-escaped IPv6 string. Try to detect that and fix it up.
|
||||
if (![self.ipv6Address containsString:@"["]) {
|
||||
self.ipv6Address = [Utils addressAndPortToAddressPortString:self.ipv6Address port:47989];
|
||||
}
|
||||
|
||||
// Ensure we don't use a stale cached pair state if we haven't pinned the cert yet
|
||||
self.pairState = host.serverCert ? [host.pairState intValue] : PairStateUnpaired;
|
||||
|
||||
|
||||
@@ -16,24 +16,15 @@
|
||||
@property (nonatomic, retain) NSNumber * framerate;
|
||||
@property (nonatomic, retain) NSNumber * height;
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * audioConfig;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
@property (nonatomic) enum {
|
||||
CODEC_PREF_AUTO,
|
||||
CODEC_PREF_H264,
|
||||
CODEC_PREF_HEVC,
|
||||
CODEC_PREF_AV1,
|
||||
} preferredCodec;
|
||||
@property (nonatomic) BOOL useFramePacing;
|
||||
@property (nonatomic) BOOL streamingRemotely;
|
||||
@property (nonatomic) BOOL useHevc;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL swapABXYButtons;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
@property (nonatomic) BOOL btMouseSupport;
|
||||
@property (nonatomic) BOOL absoluteTouchMode;
|
||||
@property (nonatomic) BOOL statsOverlay;
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
|
||||
@@ -34,17 +34,11 @@
|
||||
assert([self.bitrate intValue] != 0);
|
||||
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
|
||||
assert([self.framerate intValue] != 0);
|
||||
self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]];
|
||||
assert([self.audioConfig intValue] != 0);
|
||||
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
|
||||
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
|
||||
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"];
|
||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
|
||||
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
|
||||
self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
|
||||
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
|
||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
||||
|
||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||
switch (_screenSize) {
|
||||
@@ -60,10 +54,6 @@
|
||||
self.height = [NSNumber numberWithInteger:2160];
|
||||
self.width = [NSNumber numberWithInteger:3840];
|
||||
break;
|
||||
case 3:
|
||||
self.height = [NSNumber numberWithInteger:1440];
|
||||
self.width = [NSNumber numberWithInteger:2560];
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
}
|
||||
@@ -73,18 +63,13 @@
|
||||
self.framerate = settings.framerate;
|
||||
self.height = settings.height;
|
||||
self.width = settings.width;
|
||||
self.audioConfig = settings.audioConfig;
|
||||
self.preferredCodec = settings.preferredCodec;
|
||||
self.useFramePacing = settings.useFramePacing;
|
||||
self.useHevc = settings.useHevc;
|
||||
self.playAudioOnPC = settings.playAudioOnPC;
|
||||
self.enableHdr = settings.enableHdr;
|
||||
self.optimizeGames = settings.optimizeGames;
|
||||
self.multiController = settings.multiController;
|
||||
self.swapABXYButtons = settings.swapABXYButtons;
|
||||
self.onscreenControls = settings.onscreenControls;
|
||||
self.btMouseSupport = settings.btMouseSupport;
|
||||
self.absoluteTouchMode = settings.absoluteTouchMode;
|
||||
self.statsOverlay = settings.statsOverlay;
|
||||
#endif
|
||||
self.uniqueId = settings.uniqueId;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AltIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ControlIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "DeleteIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "doneIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 405 B |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "EscapeIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ShiftIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 719 B |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "TabIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "WindowsIcon.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// AbsoluteTouchHandler.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AbsoluteTouchHandler : UIResponder
|
||||
|
||||
-(id)initWithView:(StreamView*)view;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,120 +0,0 @@
|
||||
//
|
||||
// AbsoluteTouchHandler.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AbsoluteTouchHandler.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
// How long the fingers must be stationary to start a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
|
||||
|
||||
// How far the finger can move before it cancels a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
||||
|
||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
|
||||
|
||||
// How far the finger can move before it can override the double tap deadzone
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||
|
||||
@implementation AbsoluteTouchHandler {
|
||||
StreamView* view;
|
||||
|
||||
NSTimer* longPressTimer;
|
||||
UITouch* lastTouchDown;
|
||||
CGPoint lastTouchDownLocation;
|
||||
UITouch* lastTouchUp;
|
||||
CGPoint lastTouchUpLocation;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view {
|
||||
self = [self init];
|
||||
self->view = view;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)onLongPressStart:(NSTimer*)timer {
|
||||
// Raise the left click and start a right click
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
// Ignore touch down events with more than one finger
|
||||
if ([[event allTouches] count] > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITouch* touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:view];
|
||||
|
||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||
if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
||||
sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchUpLocation.x / view.bounds.size.width), 2) +
|
||||
pow((touchLocation.y / view.bounds.size.height) - (lastTouchUpLocation.y / view.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
||||
[view updateCursorLocation:touchLocation isMouse:NO];
|
||||
}
|
||||
|
||||
// Press the left button down
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Start the long press timer
|
||||
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
|
||||
target:self
|
||||
selector:@selector(onLongPressStart:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
|
||||
lastTouchDown = touch;
|
||||
lastTouchDownLocation = touchLocation;
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
// Ignore touch move events with more than one finger
|
||||
if ([[event allTouches] count] > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
UITouch* touch = [touches anyObject];
|
||||
CGPoint touchLocation = [touch locationInView:view];
|
||||
|
||||
if (sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchDownLocation.x / view.bounds.size.width), 2) +
|
||||
pow((touchLocation.y / view.bounds.size.height) - (lastTouchDownLocation.y / view.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||
// Moved too far since touch down. Cancel the long press timer.
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
}
|
||||
|
||||
[view updateCursorLocation:[[touches anyObject] locationInView:view] isMouse:NO];
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
// Only fire this logic if all touches have ended
|
||||
if ([[event allTouches] count] == [touches count]) {
|
||||
// Cancel the long press timer
|
||||
[longPressTimer invalidate];
|
||||
longPressTimer = nil;
|
||||
|
||||
// Left button up on finger up
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
|
||||
// Raise right button too in case we triggered a long press gesture
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
|
||||
// Remember this last touch for touch-down deadzoning
|
||||
lastTouchUp = [touches anyObject];
|
||||
lastTouchUpLocation = [lastTouchUp locationInView:view];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
// Treat this as a normal touchesEnded event
|
||||
[self touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,18 +6,10 @@
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import GameController;
|
||||
@import CoreHaptics;
|
||||
|
||||
@interface Controller : NSObject
|
||||
|
||||
typedef struct {
|
||||
float lastX;
|
||||
float lastY;
|
||||
} controller_touch_context_t;
|
||||
|
||||
@property (nullable, nonatomic, retain) GCController* gamepad;
|
||||
@property (nonatomic) int playerIndex;
|
||||
@property (nonatomic) int lastButtonFlags;
|
||||
@@ -29,25 +21,7 @@ typedef struct {
|
||||
@property (nonatomic) short lastLeftStickY;
|
||||
@property (nonatomic) short lastRightStickX;
|
||||
@property (nonatomic) short lastRightStickY;
|
||||
|
||||
@property (nonatomic) controller_touch_context_t primaryTouch;
|
||||
@property (nonatomic) controller_touch_context_t secondaryTouch;
|
||||
|
||||
@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable highFreqMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable leftTriggerMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable rightTriggerMotor;
|
||||
|
||||
@property (nonatomic) NSTimer* _Nullable accelTimer;
|
||||
@property (nonatomic) GCAcceleration lastAccelSample;
|
||||
@property (nonatomic) NSTimer* _Nullable gyroTimer;
|
||||
@property (nonatomic) GCRotationRate lastGyroSample;
|
||||
|
||||
@property (nonatomic) NSTimer* _Nullable batteryTimer;
|
||||
@property (nonatomic) GCDeviceBatteryState lastBatteryState;
|
||||
@property (nonatomic) float lastBatteryLevel;
|
||||
|
||||
@property (nonatomic) BOOL reportedArrival;
|
||||
@property (nonatomic) Controller* _Nullable mergedWithController;
|
||||
@property (nonatomic) unsigned short lowFreqMotor;
|
||||
@property (nonatomic) unsigned short highFreqMotor;
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,18 +11,15 @@
|
||||
|
||||
@class OnScreenControls;
|
||||
|
||||
@protocol ControllerSupportDelegate <NSObject>
|
||||
@protocol GamepadPresenceDelegate <NSObject>
|
||||
|
||||
- (void) gamepadPresenceChanged;
|
||||
- (void) mousePresenceChanged;
|
||||
- (void) streamExitRequested;
|
||||
|
||||
@end
|
||||
|
||||
@interface ControllerSupport : NSObject
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
|
||||
-(void) connectionEstablished;
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<GamepadPresenceDelegate>)delegate;
|
||||
|
||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
||||
-(void) cleanup;
|
||||
@@ -42,9 +39,6 @@
|
||||
-(void) updateFinished:(Controller*)controller;
|
||||
|
||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
||||
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
|
||||
-(void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
|
||||
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
|
||||
|
||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// HapticContext.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
|
||||
@interface HapticContext : NSObject
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude;
|
||||
-(void)cleanup;
|
||||
|
||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad;
|
||||
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad;
|
||||
|
||||
@end
|
||||
@@ -1,170 +0,0 @@
|
||||
//
|
||||
// HapticContext.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 9/17/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import CoreHaptics;
|
||||
@import GameController;
|
||||
|
||||
@implementation HapticContext {
|
||||
GCControllerPlayerIndex _playerIndex;
|
||||
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
|
||||
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
|
||||
BOOL _playing;
|
||||
}
|
||||
|
||||
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
if (_hapticPlayer != nil) {
|
||||
[_hapticPlayer cancelAndReturnError:nil];
|
||||
_hapticPlayer = nil;
|
||||
}
|
||||
if (_hapticEngine != nil) {
|
||||
[_hapticEngine stopWithCompletionHandler:nil];
|
||||
_hapticEngine = nil;
|
||||
}
|
||||
}
|
||||
|
||||
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
NSError* error;
|
||||
|
||||
// Check if the haptic engine died
|
||||
if (_hapticEngine == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop the effect entirely if the amplitude is 0
|
||||
if (amplitude == 0) {
|
||||
if (_playing) {
|
||||
[_hapticPlayer stopAtTime:0 error:&error];
|
||||
_playing = NO;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_hapticPlayer == nil) {
|
||||
// We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied
|
||||
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
|
||||
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
|
||||
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
|
||||
[_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_playing) {
|
||||
[_hapticPlayer startAtTime:0 error:&error];
|
||||
if (error != nil) {
|
||||
_hapticPlayer = nil;
|
||||
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
|
||||
return;
|
||||
}
|
||||
|
||||
_playing = YES;
|
||||
}
|
||||
}
|
||||
|
||||
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
|
||||
if (gamepad.haptics == nil) {
|
||||
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
|
||||
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_playerIndex = gamepad.playerIndex;
|
||||
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
|
||||
|
||||
NSError* error;
|
||||
[_hapticEngine startAndReturnError:&error];
|
||||
if (error != nil) {
|
||||
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_hapticEngine = nil;
|
||||
me->_playing = NO;
|
||||
};
|
||||
_hapticEngine.resetHandler = ^{
|
||||
HapticContext* me = weakSelf;
|
||||
if (me == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
|
||||
me->_hapticPlayer = nil;
|
||||
me->_playing = NO;
|
||||
[me->_hapticEngine startAndReturnError:nil];
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// KeyboardInputField.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/2/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface KeyboardInputField : UITextField
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// KeyboardInputField.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 12/2/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "KeyboardInputField.h"
|
||||
|
||||
@implementation KeyboardInputField
|
||||
|
||||
- (UIEditingInteractionConfiguration) editingInteractionConfiguration {
|
||||
// Suppress the Undo menu that appears with a 3 finger tap
|
||||
return UIEditingInteractionConfigurationNone;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -16,7 +16,6 @@ struct KeyEvent {
|
||||
u_char modifier;
|
||||
};
|
||||
|
||||
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4));
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
|
||||
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
|
||||
|
||||
|
||||
@@ -11,40 +11,7 @@
|
||||
|
||||
@implementation KeyboardSupport
|
||||
|
||||
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4)) {
|
||||
if (press.key != nil) {
|
||||
return [KeyboardSupport sendKeyEvent:press.key down:down];
|
||||
}
|
||||
else {
|
||||
short keyCode;
|
||||
|
||||
switch (press.type) {
|
||||
case UIPressTypeUpArrow:
|
||||
keyCode = 0x26;
|
||||
break;
|
||||
case UIPressTypeDownArrow:
|
||||
keyCode = 0x28;
|
||||
break;
|
||||
case UIPressTypeLeftArrow:
|
||||
keyCode = 0x25;
|
||||
break;
|
||||
case UIPressTypeRightArrow:
|
||||
keyCode = 0x27;
|
||||
break;
|
||||
default:
|
||||
// Unhandled press type
|
||||
return NO;
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
0);
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)) {
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)){
|
||||
char modifierFlags = 0;
|
||||
short keyCode = 0;
|
||||
|
||||
@@ -54,12 +21,9 @@
|
||||
if (key.modifierFlags & UIKeyModifierAlternate) {
|
||||
modifierFlags |= MODIFIER_ALT;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierControl) {
|
||||
if (key.modifierFlags & (UIKeyModifierControl | UIKeyModifierCommand)) {
|
||||
modifierFlags |= MODIFIER_CTRL;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierCommand) {
|
||||
modifierFlags |= MODIFIER_META;
|
||||
}
|
||||
|
||||
// This converts UIKeyboardHIDUsage values to Win32 VK_* values
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
@@ -201,8 +165,7 @@
|
||||
keyCode = 0x6B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadEnter:
|
||||
keyCode = 0x0D;
|
||||
modifierFlags |= MODIFIER_EXTENDED;
|
||||
keyCode = 0x0D; // FIXME: Is this correct?
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadPeriod:
|
||||
keyCode = 0x6E;
|
||||
@@ -226,8 +189,6 @@
|
||||
keyCode = 0xF8;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftGUI:
|
||||
keyCode = 0x5B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftControl:
|
||||
keyCode = 0xA2;
|
||||
break;
|
||||
@@ -238,8 +199,6 @@
|
||||
keyCode = 0xA4;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightGUI:
|
||||
keyCode = 0x5C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightControl:
|
||||
keyCode = 0xA3;
|
||||
break;
|
||||
@@ -249,9 +208,6 @@
|
||||
case UIKeyboardHIDUsageKeyboardRightAlt:
|
||||
keyCode = 0xA5;
|
||||
break;
|
||||
case 669: // This value corresponds to the "Globe" or "Language" key on most Apple branded iPad keyboards.
|
||||
keyCode = 0x1B; // This value corresponds to "Escape", which is missing from most Apple branded iPad keyboards.
|
||||
break;
|
||||
default:
|
||||
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
|
||||
assert(0);
|
||||
@@ -259,7 +215,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
LiSendKeyboardEvent(keyCode,
|
||||
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
modifierFlags);
|
||||
return true;
|
||||
@@ -276,12 +232,10 @@
|
||||
case UIKeyModifierShift:
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierCommand:
|
||||
case UIKeyModifierControl:
|
||||
[KeyboardSupport addControlModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierCommand:
|
||||
[KeyboardSupport addMetaModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierAlternate:
|
||||
[KeyboardSupport addAltModifier:&event];
|
||||
break;
|
||||
@@ -439,11 +393,6 @@
|
||||
event->modifierKeycode = 0x11;
|
||||
}
|
||||
|
||||
+ (void) addMetaModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_META;
|
||||
event->modifierKeycode = 0x5B;
|
||||
}
|
||||
|
||||
+ (void) addAltModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_ALT;
|
||||
event->modifierKeycode = 0x12;
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol EdgeDetectionDelegate;
|
||||
@class ControllerSupport;
|
||||
@class StreamConfiguration;
|
||||
|
||||
@interface OnScreenControls : NSObject
|
||||
|
||||
@@ -25,7 +25,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
|
||||
};
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig;
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
|
||||
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
CALayer* _l1Button;
|
||||
CALayer* _l2Button;
|
||||
CALayer* _l3Button;
|
||||
CALayer* _edge;
|
||||
|
||||
UITouch* _aTouch;
|
||||
UITouch* _bTouch;
|
||||
@@ -52,6 +53,7 @@
|
||||
UITouch* _l1Touch;
|
||||
UITouch* _l2Touch;
|
||||
UITouch* _l3Touch;
|
||||
UITouch* _edgeTouch;
|
||||
|
||||
NSDate* l3TouchStart;
|
||||
NSDate* r3TouchStart;
|
||||
@@ -67,8 +69,8 @@
|
||||
|
||||
ControllerSupport *_controllerSupport;
|
||||
Controller *_controller;
|
||||
id<EdgeDetectionDelegate> _edgeDelegate;
|
||||
NSMutableArray* _deadTouches;
|
||||
BOOL _swapABXY;
|
||||
}
|
||||
|
||||
static const float EDGE_WIDTH = .05;
|
||||
@@ -112,13 +114,13 @@ static float L2_Y;
|
||||
static float L3_X;
|
||||
static float L3_Y;
|
||||
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig {
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate {
|
||||
self = [self init];
|
||||
_view = view;
|
||||
_controllerSupport = controllerSupport;
|
||||
_controller = [controllerSupport getOscController];
|
||||
_edgeDelegate = swipeDelegate;
|
||||
_deadTouches = [[NSMutableArray alloc] init];
|
||||
_swapABXY = streamConfig.swapABXYButtons;
|
||||
|
||||
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
|
||||
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
|
||||
@@ -154,13 +156,16 @@ static float L3_Y;
|
||||
_rightStickBackground = [CALayer layer];
|
||||
_leftStick = [CALayer layer];
|
||||
_rightStick = [CALayer layer];
|
||||
_edge = [CALayer layer];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) show {
|
||||
_visible = YES;
|
||||
|
||||
|
||||
[self setupEdgeDetection];
|
||||
|
||||
[self updateControls];
|
||||
}
|
||||
|
||||
@@ -249,6 +254,11 @@ static float L3_Y;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setupEdgeDetection {
|
||||
_edge.frame = CGRectMake(0, 0, _view.frame.size.width * EDGE_WIDTH, _view.frame.size.height);
|
||||
[_view.layer addSublayer:_edge];
|
||||
}
|
||||
|
||||
// For GCExtendedGamepad controls we move start, select, L3, and R3 to the button
|
||||
- (void) setupExtendedGamepadControls {
|
||||
// Start with the default complex layout
|
||||
@@ -374,33 +384,27 @@ static float L3_Y;
|
||||
}
|
||||
|
||||
- (void) drawButtons {
|
||||
UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
|
||||
UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
|
||||
UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
|
||||
UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
|
||||
|
||||
CGRect aButtonFrame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
|
||||
CGRect bButtonFrame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
|
||||
CGRect xButtonFrame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
|
||||
CGRect yButtonFrame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
|
||||
|
||||
// create A button
|
||||
UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
|
||||
_aButton.contents = (id) aButtonImage.CGImage;
|
||||
_aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame;
|
||||
_aButton.frame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
|
||||
[_view.layer addSublayer:_aButton];
|
||||
|
||||
// create B button
|
||||
_bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame;
|
||||
UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
|
||||
_bButton.frame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
|
||||
_bButton.contents = (id) bButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_bButton];
|
||||
|
||||
// create X Button
|
||||
_xButton.frame = _swapABXY ? yButtonFrame : xButtonFrame;
|
||||
UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
|
||||
_xButton.frame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
|
||||
_xButton.contents = (id) xButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_xButton];
|
||||
|
||||
// create Y Button
|
||||
_yButton.frame = _swapABXY ? xButtonFrame : yButtonFrame;
|
||||
UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
|
||||
_yButton.frame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
|
||||
_yButton.contents = (id) yButtonImage.CGImage;
|
||||
[_view.layer addSublayer:_yButton];
|
||||
|
||||
@@ -654,7 +658,7 @@ static float L3_Y;
|
||||
if (updated) {
|
||||
[_controllerSupport updateFinished:_controller];
|
||||
}
|
||||
return updated || buttonTouch;
|
||||
return updated || buttonTouch;
|
||||
}
|
||||
|
||||
- (BOOL)handleTouchDownEvent:touches {
|
||||
@@ -663,63 +667,63 @@ static float L3_Y;
|
||||
for (UITouch* touch in touches) {
|
||||
CGPoint touchLocation = [touch locationInView:_view];
|
||||
|
||||
if (_aButton.superlayer != nil && [_aButton.presentationLayer hitTest:touchLocation]) {
|
||||
if ([_aButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:A_FLAG];
|
||||
_aTouch = touch;
|
||||
updated = true;
|
||||
} else if (_bButton.superlayer != nil && [_bButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_bButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:B_FLAG];
|
||||
_bTouch = touch;
|
||||
updated = true;
|
||||
} else if (_xButton.superlayer != nil && [_xButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_xButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:X_FLAG];
|
||||
_xTouch = touch;
|
||||
updated = true;
|
||||
} else if (_yButton.superlayer != nil && [_yButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_yButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:Y_FLAG];
|
||||
_yTouch = touch;
|
||||
updated = true;
|
||||
} else if (_upButton.superlayer != nil && [_upButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_upButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_downButton.superlayer != nil && [_downButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_downButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_leftButton.superlayer != nil && [_leftButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_leftButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_rightButton.superlayer != nil && [_rightButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_rightButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
|
||||
_dpadTouch = touch;
|
||||
updated = true;
|
||||
} else if (_startButton.superlayer != nil && [_startButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_startButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG];
|
||||
_startTouch = touch;
|
||||
updated = true;
|
||||
} else if (_selectButton.superlayer != nil && [_selectButton.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_selectButton.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:BACK_FLAG];
|
||||
_selectTouch = touch;
|
||||
updated = true;
|
||||
} else if (_l1Button.superlayer != nil && [_l1Button.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_l1Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:LB_FLAG];
|
||||
_l1Touch = touch;
|
||||
updated = true;
|
||||
} else if (_r1Button.superlayer != nil && [_r1Button.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_r1Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport setButtonFlag:_controller flags:RB_FLAG];
|
||||
_r1Touch = touch;
|
||||
updated = true;
|
||||
} else if (_l2Button.superlayer != nil && [_l2Button.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_l2Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport updateLeftTrigger:_controller left:0xFF];
|
||||
_l2Touch = touch;
|
||||
updated = true;
|
||||
} else if (_r2Button.superlayer != nil && [_r2Button.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_r2Button.presentationLayer hitTest:touchLocation]) {
|
||||
[_controllerSupport updateRightTrigger:_controller right:0xFF];
|
||||
_r2Touch = touch;
|
||||
updated = true;
|
||||
} else if (_l3Button.superlayer != nil && [_l3Button.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_l3Button.presentationLayer hitTest:touchLocation]) {
|
||||
if (l3Set) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
|
||||
_l3Button.borderWidth = 0.0f;
|
||||
@@ -730,7 +734,7 @@ static float L3_Y;
|
||||
l3Set = !l3Set;
|
||||
_l3Touch = touch;
|
||||
updated = true;
|
||||
} else if (_r3Button.superlayer != nil && [_r3Button.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_r3Button.presentationLayer hitTest:touchLocation]) {
|
||||
if (r3Set) {
|
||||
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
|
||||
_r3Button.borderWidth = 0.0f;
|
||||
@@ -741,7 +745,7 @@ static float L3_Y;
|
||||
r3Set = !r3Set;
|
||||
_r3Touch = touch;
|
||||
updated = true;
|
||||
} else if (_leftStick.superlayer != nil && [_leftStick.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_leftStick.presentationLayer hitTest:touchLocation]) {
|
||||
if (l3TouchStart != nil) {
|
||||
// Find elapsed time and convert to milliseconds
|
||||
// Use (-) modifier to conversion since receiver is earlier than now
|
||||
@@ -753,7 +757,7 @@ static float L3_Y;
|
||||
}
|
||||
_lsTouch = touch;
|
||||
stickTouch = true;
|
||||
} else if (_rightStick.superlayer != nil && [_rightStick.presentationLayer hitTest:touchLocation]) {
|
||||
} else if ([_rightStick.presentationLayer hitTest:touchLocation]) {
|
||||
if (r3TouchStart != nil) {
|
||||
// Find elapsed time and convert to milliseconds
|
||||
// Use (-) modifier to conversion since receiver is earlier than now
|
||||
@@ -765,6 +769,8 @@ static float L3_Y;
|
||||
}
|
||||
_rsTouch = touch;
|
||||
stickTouch = true;
|
||||
} else if ([_edge.presentationLayer hitTest:touchLocation]) {
|
||||
_edgeTouch = touch;
|
||||
}
|
||||
if (!updated && !stickTouch && [self isInDeadZone:touch]) {
|
||||
[_deadTouches addObject:touch];
|
||||
@@ -848,6 +854,11 @@ static float L3_Y;
|
||||
else if (touch == _r3Touch) {
|
||||
_r3Touch = nil;
|
||||
touched = true;
|
||||
} else if (touch == _edgeTouch) {
|
||||
_edgeTouch = nil;
|
||||
if ([touch locationInView:_view].x >= _view.frame.size.width / 4) {
|
||||
[_edgeDelegate edgeSwiped];
|
||||
}
|
||||
}
|
||||
if ([_deadTouches containsObject:touch]) {
|
||||
[_deadTouches removeObject:touch];
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//
|
||||
// RelativeTouchHandler.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RelativeTouchHandler : UIResponder
|
||||
|
||||
-(id)initWithView:(StreamView*)view;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,207 +0,0 @@
|
||||
//
|
||||
// RelativeTouchHandler.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RelativeTouchHandler.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
static const int REFERENCE_WIDTH = 1280;
|
||||
static const int REFERENCE_HEIGHT = 720;
|
||||
|
||||
@implementation RelativeTouchHandler {
|
||||
CGPoint touchLocation, originalLocation;
|
||||
BOOL touchMoved;
|
||||
BOOL isDragging;
|
||||
NSTimer* dragTimer;
|
||||
NSUInteger peakTouchCount;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIGestureRecognizer* remotePressRecognizer;
|
||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||
#endif
|
||||
|
||||
UIView* view;
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view {
|
||||
self = [self init];
|
||||
self->view = view;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)];
|
||||
remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
[self->view addGestureRecognizer:remotePressRecognizer];
|
||||
[self->view addGestureRecognizer:remoteLongPressRecognizer];
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||
// Movements of greater than 5 pixels are considered confirmed
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 5;
|
||||
}
|
||||
|
||||
- (void)onDragStart:(NSTimer*)timer {
|
||||
if (!touchMoved && !isDragging){
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
touchMoved = false;
|
||||
peakTouchCount = [[event allTouches] count];
|
||||
if ([[event allTouches] count] == 1) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
originalLocation = touchLocation = [touch locationInView:view];
|
||||
if (!isDragging) {
|
||||
dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650
|
||||
target:self
|
||||
selector:@selector(onDragStart:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
}
|
||||
else if ([[event allTouches] count] == 2) {
|
||||
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view];
|
||||
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view];
|
||||
|
||||
originalLocation = touchLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if ([[event allTouches] count] == 1) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
CGPoint currentLocation = [touch locationInView:view];
|
||||
|
||||
if (touchLocation.x != currentLocation.x ||
|
||||
touchLocation.y != currentLocation.y)
|
||||
{
|
||||
int deltaX = (currentLocation.x - touchLocation.x) * (REFERENCE_WIDTH / view.bounds.size.width);
|
||||
int deltaY = (currentLocation.y - touchLocation.y) * (REFERENCE_HEIGHT / view.bounds.size.height);
|
||||
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
touchLocation = currentLocation;
|
||||
|
||||
// If we've moved far enough to confirm this wasn't just human/machine error,
|
||||
// mark it as such.
|
||||
if ([self isConfirmedMove:touchLocation from:originalLocation]) {
|
||||
touchMoved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ([[event allTouches] count] == 2) {
|
||||
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view];
|
||||
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view];
|
||||
|
||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
if (touchLocation.y != avgLocation.y) {
|
||||
LiSendHighResScrollEvent((avgLocation.y - touchLocation.y) * 10);
|
||||
}
|
||||
|
||||
// If we've moved far enough to confirm this wasn't just human/machine error,
|
||||
// mark it as such.
|
||||
if ([self isConfirmedMove:firstLocation from:originalLocation]) {
|
||||
touchMoved = true;
|
||||
}
|
||||
|
||||
touchLocation = avgLocation;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
} else if (!touchMoved) {
|
||||
if (peakTouchCount == 2) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
} else if (peakTouchCount == 1) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
self->isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We we're moving from 2+ touches to 1. Synchronize the current position
|
||||
// of the active finger so we don't jump unexpectedly on the next touchesMoved
|
||||
// callback when finger 1 switches on us.
|
||||
if ([[event allTouches] count] - [touches count] == 1) {
|
||||
NSMutableSet *activeSet = [[NSMutableSet alloc] initWithCapacity:[[event allTouches] count]];
|
||||
[activeSet unionSet:[event allTouches]];
|
||||
[activeSet minusSet:touches];
|
||||
touchLocation = [[activeSet anyObject] locationInView:view];
|
||||
|
||||
// Mark this touch as moved so we don't send a left mouse click if the user
|
||||
// right clicks without moving their other finger.
|
||||
touchMoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
peakTouchCount = 0;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)remoteButtonPressed:(id)sender {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
// Mark this as touchMoved to avoid a duplicate press on touch up
|
||||
self->touchMoved = true;
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
- (void)remoteButtonLongPressed:(id)sender {
|
||||
Log(LOG_D, @"Holding left mouse button");
|
||||
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -8,9 +8,14 @@
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol EdgeDetectionDelegate <NSObject>
|
||||
|
||||
- (void) edgeSwiped;
|
||||
|
||||
@end
|
||||
|
||||
@protocol UserInteractionDelegate <NSObject>
|
||||
|
||||
- (void) userInteractionBegan;
|
||||
@@ -19,19 +24,18 @@
|
||||
@end
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
||||
@interface StreamView : OSView <UITextFieldDelegate>
|
||||
#else
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
@interface StreamView : OSView <UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
#endif
|
||||
|
||||
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
|
||||
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig;
|
||||
- (void) showOnScreenControls;
|
||||
- (OnScreenControlsLevel) getCurrentOscState;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDuration</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -32,13 +30,13 @@
|
||||
<key>ProfileName</key>
|
||||
<string>ExtendedGamepad</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>Gamepad</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GCSupportsControllerUserInteraction</key>
|
||||
<true/>
|
||||
<key>LSSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
@@ -48,16 +46,12 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
|
||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_nvstream._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
@@ -21,4 +21,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#import "Logger.h"
|
||||
#include "OSPortabilityDefs.h"
|
||||
#endif
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Moonlight v1.10.xcdatamodel</string>
|
||||
<string>Moonlight v1.6.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23A344" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
</model>
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19H2" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19G2021" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
@@ -23,7 +22,6 @@
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
@@ -31,14 +29,13 @@
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="268"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="223"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="20G224" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="254"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="298"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,47 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="useHevc2" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="118"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="298"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -16,8 +16,8 @@
|
||||
self.statusMessage = @"App asset has no status message";
|
||||
self.statusCode = -1;
|
||||
}
|
||||
- (UIImage*) getImage {
|
||||
return [[UIImage alloc] initWithData:self.data];
|
||||
- (OSImage*) getImage {
|
||||
return [[OSImage alloc] initWithData:self.data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -20,7 +20,7 @@ static const int MAX_ATTEMPTS = 5;
|
||||
- (void) main {
|
||||
int attempts = 0;
|
||||
while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host];
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] serverCert:_host.serverCert];
|
||||
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
//
|
||||
|
||||
#import "AppListResponse.h"
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
#ifndef ConnectionHelper_h
|
||||
#define ConnectionHelper_h
|
||||
|
||||
|
||||
@interface ConnectionHelper : NSObject
|
||||
|
||||
+(AppListResponse*) getAppListForHost:(TemporaryHost*)host;
|
||||
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) serverCert uniqueID:(NSString*) uniqueId;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
@implementation ConnectionHelper
|
||||
|
||||
+(AppListResponse*) getAppListForHost:(TemporaryHost*)host {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
|
||||
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) cert uniqueID:(NSString*) uniqueId {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
|
||||
|
||||
// Try up to 5 times to get the app list
|
||||
AppListResponse* appListResp = nil;
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
}
|
||||
|
||||
- (ServerInfoResponse*) getServerInfoResponseForAddress:(NSString*)address {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithAddress:address httpsPort:0 serverCert:nil];
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:address uniqueId:_uniqueId serverCert:nil];
|
||||
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
return serverInfoResponse;
|
||||
@@ -202,7 +202,7 @@
|
||||
callback(host, nil);
|
||||
}
|
||||
} else if (!prohibitedAddress) {
|
||||
callback(nil, @"Could not connect to host.\n\nIf you're hosting using GeForce Experience, make sure you've enabled the toggle on the SHIELD tab.\n\nIf you're hosting using Sunshine, ensure it is running properly. If you're using a non-default port, you will need to include that here.");
|
||||
callback(nil, @"Could not connect to host. Ensure GameStream is enabled in GeForce Experience on your PC.");
|
||||
} else {
|
||||
callback(nil, prohibitedAddressMessage);
|
||||
}
|
||||
|
||||
@@ -103,8 +103,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
|
||||
receivedResponse = [self checkResponse:serverInfoResp];
|
||||
if (receivedResponse) {
|
||||
_host.activeAddress = address;
|
||||
[serverInfoResp populateHost:_host];
|
||||
_host.activeAddress = address;
|
||||
|
||||
// Update the database using the response
|
||||
DataManager *dataManager = [[DataManager alloc] init];
|
||||
@@ -126,7 +126,9 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
}
|
||||
|
||||
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address cert:(NSData*)cert {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithAddress:address httpsPort:0 serverCert:cert];
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:address
|
||||
uniqueId:_uniqueId
|
||||
serverCert:cert];
|
||||
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
|
||||
withUrlRequest:[hMan newServerInfoRequest:true]
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
#import "HttpResponse.h"
|
||||
#import "HttpRequest.h"
|
||||
#import "StreamConfiguration.h"
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
@interface HttpManager : NSObject <NSURLSessionDelegate>
|
||||
|
||||
- (id) initWithHost:(TemporaryHost*) host;
|
||||
- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short) httpsPort serverCert:(NSData*) serverCert;
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert;
|
||||
- (void) setServerCert:(NSData*) serverCert;
|
||||
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
|
||||
- (NSURLRequest*) newUnpairRequest;
|
||||
@@ -26,7 +24,8 @@
|
||||
- (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
|
||||
- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
|
||||
- (NSURLRequest*) newHttpServerInfoRequest;
|
||||
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config;
|
||||
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config;
|
||||
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config;
|
||||
- (NSURLRequest*) newQuitAppRequest;
|
||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
||||
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#import "HttpRequest.h"
|
||||
#import "CryptoManager.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "ServerInfoResponse.h"
|
||||
|
||||
#include <libxml2/libxml/xmlreader.h>
|
||||
#include <string.h>
|
||||
@@ -23,16 +22,22 @@
|
||||
#define EXTRA_LONG_TIMEOUT_SEC 180
|
||||
|
||||
@implementation HttpManager {
|
||||
NSString* _urlSafeHostName;
|
||||
NSURLSession* _urlSession;
|
||||
NSString* _baseHTTPURL;
|
||||
NSString* _baseHTTPSURL;
|
||||
NSString* _uniqueId;
|
||||
NSString* _deviceName;
|
||||
NSData* _serverCert;
|
||||
NSMutableData* _respData;
|
||||
NSData* _requestResp;
|
||||
dispatch_semaphore_t _requestLock;
|
||||
|
||||
TemporaryHost *_host; // May be nil
|
||||
NSString* _baseHTTPSURL;
|
||||
NSError* _error;
|
||||
}
|
||||
|
||||
static const NSString* HTTP_PORT = @"47989";
|
||||
static const NSString* HTTPS_PORT = @"47984";
|
||||
|
||||
+ (NSData*) fixXmlVersion:(NSData*) xmlData {
|
||||
NSString* dataString = [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding];
|
||||
NSString* xmlString = [dataString stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [dataString length])];
|
||||
@@ -44,111 +49,63 @@
|
||||
_serverCert = serverCert;
|
||||
}
|
||||
|
||||
- (id) initWithHost:(TemporaryHost*) host {
|
||||
self = [self initWithAddress:host.activeAddress httpsPort:host.httpsPort serverCert:host.serverCert];
|
||||
_host = host;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short)httpsPort serverCert:(NSData*) serverCert {
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert {
|
||||
self = [super init];
|
||||
// Use the same UID for all Moonlight clients to allow them
|
||||
// quit games started on another Moonlight client.
|
||||
_uniqueId = @"0123456789ABCDEF";
|
||||
_deviceName = deviceName;
|
||||
_serverCert = serverCert;
|
||||
|
||||
NSString* address = [Utils addressPortStringToAddress:hostAddressPortString];
|
||||
unsigned short port = [Utils addressPortStringToPort:hostAddressPortString];
|
||||
_requestLock = dispatch_semaphore_create(0);
|
||||
_respData = [[NSMutableData alloc] init];
|
||||
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
|
||||
|
||||
// If this is an IPv6 literal, we must properly enclose it in brackets
|
||||
if ([address containsString:@":"]) {
|
||||
_urlSafeHostName = [NSString stringWithFormat:@"[%@]", address];
|
||||
NSString* urlSafeHost;
|
||||
if ([host containsString:@":"]) {
|
||||
urlSafeHost = [NSString stringWithFormat:@"[%@]", host];
|
||||
} else {
|
||||
_urlSafeHostName = address;
|
||||
urlSafeHost = host;
|
||||
}
|
||||
|
||||
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%u", _urlSafeHostName, port];
|
||||
|
||||
if (httpsPort) {
|
||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, httpsPort];
|
||||
}
|
||||
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", urlSafeHost, HTTP_PORT];
|
||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", urlSafeHost, HTTPS_PORT];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL) ensureHttpsUrlPopulated:(bool)fastFail {
|
||||
if (!_baseHTTPSURL) {
|
||||
// Use the caller's provided port if one was specified
|
||||
if (_host && _host.httpsPort != 0) {
|
||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, _host.httpsPort];
|
||||
}
|
||||
else {
|
||||
// Query the host to retrieve the HTTPS port
|
||||
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
|
||||
[self executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[self newHttpServerInfoRequest:false]]];
|
||||
TemporaryHost* dummyHost = [[TemporaryHost alloc] init];
|
||||
if (![serverInfoResponse isStatusOk]) {
|
||||
return NO;
|
||||
}
|
||||
[serverInfoResponse populateHost:dummyHost];
|
||||
|
||||
// Pass the port back if the caller provided storage for it
|
||||
if (_host) {
|
||||
_host.httpsPort = dummyHost.httpsPort;
|
||||
}
|
||||
|
||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, dummyHost.httpsPort];
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) executeRequestSynchronously:(HttpRequest*)request {
|
||||
// This is a special case to handle failure of HTTPS port fetching
|
||||
if (!request.request) {
|
||||
if (request.response) {
|
||||
request.response.statusCode = EHOSTDOWN;
|
||||
request.response.statusMessage = @"Host is unreachable";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__block NSData* requestResp = nil;
|
||||
__block NSError* respError = nil;
|
||||
__block dispatch_semaphore_t requestLock = dispatch_semaphore_create(0);
|
||||
[_respData setLength:0];
|
||||
_error = nil;
|
||||
|
||||
Log(LOG_D, @"Making Request: %@", request);
|
||||
NSURLSession* urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:self delegateQueue:nil];
|
||||
[[urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
|
||||
[[_urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
|
||||
|
||||
if (error != NULL) {
|
||||
Log(LOG_D, @"Connection error: %@", error);
|
||||
respError = error;
|
||||
self->_error = error;
|
||||
}
|
||||
else {
|
||||
Log(LOG_D, @"Received response: %@", response);
|
||||
|
||||
if (data != NULL) {
|
||||
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
|
||||
if ([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] != nil) {
|
||||
requestResp = [HttpManager fixXmlVersion:data];
|
||||
[self->_respData appendData:data];
|
||||
if ([[NSString alloc] initWithData:self->_respData encoding:NSUTF8StringEncoding] != nil) {
|
||||
self->_requestResp = [HttpManager fixXmlVersion:self->_respData];
|
||||
} else {
|
||||
requestResp = data;
|
||||
self->_requestResp = self->_respData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_semaphore_signal(requestLock);
|
||||
dispatch_semaphore_signal(self->_requestLock);
|
||||
}] resume];
|
||||
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
|
||||
|
||||
dispatch_semaphore_wait(requestLock, DISPATCH_TIME_FOREVER);
|
||||
[urlSession invalidateAndCancel];
|
||||
|
||||
if (!respError && request.response) {
|
||||
[request.response populateWithData:requestResp];
|
||||
if (!_error && request.response) {
|
||||
[request.response populateWithData:_requestResp];
|
||||
|
||||
// If the fallback error code was detected, issue the fallback request
|
||||
if (request.response.statusCode == request.fallbackError && request.fallbackRequest != NULL) {
|
||||
@@ -159,7 +116,7 @@
|
||||
[self executeRequestSynchronously:request];
|
||||
}
|
||||
}
|
||||
else if (respError && [respError code] == NSURLErrorServerCertificateUntrusted) {
|
||||
else if (_error && [_error code] == NSURLErrorServerCertificateUntrusted) {
|
||||
// We must have a pinned cert for HTTPS. If we fail, it must be due to
|
||||
// a non-matching cert, not because we had no cert at all.
|
||||
assert(_serverCert != nil);
|
||||
@@ -174,9 +131,9 @@
|
||||
[self executeRequestSynchronously:request];
|
||||
}
|
||||
}
|
||||
else if (respError && request.response) {
|
||||
request.response.statusCode = [respError code];
|
||||
request.response.statusMessage = [respError localizedDescription];
|
||||
else if (_error && request.response) {
|
||||
request.response.statusCode = [_error code];
|
||||
request.response.statusMessage = [_error localizedDescription];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,19 +174,11 @@
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newPairChallenge {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newAppListRequest {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
@@ -240,10 +189,6 @@
|
||||
return [self newHttpServerInfoRequest:fastFail];
|
||||
}
|
||||
|
||||
if (![self ensureHttpsUrlPopulated:fastFail]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
|
||||
}
|
||||
@@ -257,49 +202,44 @@
|
||||
return [self newHttpServerInfoRequest:false];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3. We do not do this hack for Sunshine (which is
|
||||
// indicated by a negative version in the last field.
|
||||
int fps = (config.frameRate > 60 && ![config.appVersion containsString:@".-"]) ? 0 : config.frameRate;
|
||||
// on GFE 3.20.3.
|
||||
int fps = config.frameRate > 60 ? 0 : config.frameRate;
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/%@?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d&gcpersist=%d%s",
|
||||
_baseHTTPSURL, verb, _uniqueId,
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d",
|
||||
_baseHTTPSURL, _uniqueId,
|
||||
config.appID,
|
||||
config.width, config.height, fps,
|
||||
config.optimizeGameSettings ? 1 : 0,
|
||||
[Utils bytesToHex:config.riKey], config.riKeyId,
|
||||
(config.supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
|
||||
config.enableHdr ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
|
||||
config.playAudioOnPC ? 1 : 0,
|
||||
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration),
|
||||
config.gamepadMask, config.gamepadMask,
|
||||
!config.multiController ? 1 : 0,
|
||||
LiGetLaunchUrlQueryParameters()];
|
||||
config.gamepadMask, config.gamepadMask];
|
||||
Log(LOG_I, @"Requesting: %@", urlString);
|
||||
// This blocks while the app is launching
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
|
||||
_baseHTTPSURL, _uniqueId,
|
||||
[Utils bytesToHex:config.riKey], config.riKeyId,
|
||||
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration)];
|
||||
Log(LOG_I, @"Requesting: %@", urlString);
|
||||
// This blocks while the app is resuming
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newQuitAppRequest {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
|
||||
if (![self ensureHttpsUrlPopulated:NO]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
@@ -319,7 +259,7 @@
|
||||
|
||||
SecIdentityCopyCertificate(identity, &certificate);
|
||||
|
||||
return [[NSArray alloc] initWithObjects:(__bridge_transfer id)certificate, nil];
|
||||
return [[NSArray alloc] initWithObjects:(__bridge id)certificate, nil];
|
||||
}
|
||||
|
||||
// Returns the identity
|
||||
@@ -331,14 +271,13 @@
|
||||
const void *keys[] = { kSecImportExportPassphrase };
|
||||
const void *values[] = { password };
|
||||
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
|
||||
CFArrayRef items = nil;
|
||||
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
|
||||
OSStatus securityError = SecPKCS12Import(p12Data, options, &items);
|
||||
|
||||
if (securityError == errSecSuccess) {
|
||||
//Log(LOG_D, @"Success opening p12 certificate. Items: %ld", CFArrayGetCount(items));
|
||||
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
|
||||
identityApp = (SecIdentityRef)CFRetain(CFDictionaryGetValue(identityDict, kSecImportItemIdentity));
|
||||
CFRelease(items);
|
||||
identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
|
||||
} else {
|
||||
Log(LOG_E, @"Error opening Certificate.");
|
||||
}
|
||||
@@ -392,7 +331,6 @@
|
||||
SecIdentityRef identity = [self getClientCertificate];
|
||||
NSArray* certArray = [self getCertificate:identity];
|
||||
NSURLCredential* newCredential = [NSURLCredential credentialWithIdentity:identity certificates:certArray persistence:NSURLCredentialPersistencePermanent];
|
||||
CFRelease(identity);
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -72,21 +72,16 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
|
||||
if (addr->sa_family == AF_INET) {
|
||||
inet_ntop(addr->sa_family, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr));
|
||||
unsigned short port = ntohs(((struct sockaddr_in*)addr)->sin_port);
|
||||
return [NSString stringWithFormat: @"%s:%u", addrStr, port];
|
||||
}
|
||||
else {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
|
||||
inet_ntop(addr->sa_family, &sin6->sin6_addr, addrStr, sizeof(addrStr));
|
||||
unsigned short port = ntohs(((struct sockaddr_in6*)addr)->sin6_port);
|
||||
if (sin6->sin6_scope_id != 0) {
|
||||
// Link-local addresses with scope IDs are special
|
||||
return [NSString stringWithFormat: @"[%s%%%u]:%u", addrStr, sin6->sin6_scope_id, port];
|
||||
}
|
||||
else {
|
||||
return [NSString stringWithFormat: @"[%s]:%u", addrStr, port];
|
||||
return [NSString stringWithFormat: @"%s%%%u", addrStr, sin6->sin6_scope_id];
|
||||
}
|
||||
}
|
||||
return [NSString stringWithFormat: @"%s", addrStr];
|
||||
}
|
||||
|
||||
+ (BOOL)isAddress:(uint8_t*)address inSubnet:(uint8_t*)subnet netmask:(int)bits {
|
||||
@@ -240,7 +235,7 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
[NSTimer scheduledTimerWithTimeInterval:2.0
|
||||
target:self
|
||||
selector:@selector(retryResolveTimerCallback:)
|
||||
userInfo:sender
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
@@ -294,12 +289,12 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
return;
|
||||
}
|
||||
|
||||
NSNetService* service = timer.userInfo;
|
||||
Log(LOG_I, @"Retrying mDNS resolution for %@", service);
|
||||
|
||||
if (service.hostName == nil) {
|
||||
[service setDelegate:self];
|
||||
[service resolveWithTimeout:5];
|
||||
Log(LOG_I, @"Retrying mDNS resolution");
|
||||
for (NSNetService* service in services) {
|
||||
if (service.hostName == nil) {
|
||||
[service setDelegate:self];
|
||||
[service resolveWithTimeout:5];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,15 @@
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
|
||||
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
|
||||
if ([serverInfoResp isStatusOk]) {
|
||||
if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
|
||||
if ([[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
|
||||
[_callback pairFailed:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
|
||||
} else if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
|
||||
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
|
||||
if (appversion == nil) {
|
||||
[_callback pairFailed:@"Missing XML element"];
|
||||
return;
|
||||
}
|
||||
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue] withState:[serverInfoResp getStringTag:@"state"]];
|
||||
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue]];
|
||||
} else {
|
||||
[_callback alreadyPaired];
|
||||
}
|
||||
@@ -80,8 +82,8 @@
|
||||
}
|
||||
|
||||
// All codepaths must call finishPairing exactly once before returning!
|
||||
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion withState:(NSString*)state {
|
||||
Log(LOG_I, @"Pairing with generation %d server in state %@", serverMajorVersion, state);
|
||||
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion {
|
||||
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
|
||||
|
||||
// Start a background task to help prevent the app from being killed
|
||||
// while pairing is in progress.
|
||||
@@ -97,13 +99,7 @@
|
||||
HttpResponse* pairResp = [[HttpResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
|
||||
if (![self verifyResponseStatus:pairResp]) {
|
||||
// GFE does not allow pairing while a server is busy, but Sunshine does. We give it a try and display the busy error if it fails.
|
||||
if ([state hasSuffix:@"_SERVER_BUSY"]) {
|
||||
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
|
||||
}
|
||||
else {
|
||||
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
|
||||
}
|
||||
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,7 +168,7 @@
|
||||
|
||||
NSData* serverSecretResp = [Utils hexToBytes:[secretResp getStringTag:@"pairingsecret"]];
|
||||
NSData* serverSecret = [serverSecretResp subdataWithRange:NSMakeRange(0, 16)];
|
||||
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, serverSecretResp.length - 16)];
|
||||
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, 256)];
|
||||
|
||||
if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
|
||||
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"];
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#define TAG_HOSTNAME @"hostname"
|
||||
#define TAG_EXTERNAL_IP @"ExternalIP"
|
||||
#define TAG_HTTPS_PORT @"HttpsPort"
|
||||
#define TAG_LOCAL_IP @"LocalIP"
|
||||
#define TAG_UNIQUE_ID @"uniqueid"
|
||||
#define TAG_MAC_ADDRESS @"mac"
|
||||
@@ -19,9 +18,6 @@
|
||||
#define TAG_STATE @"state"
|
||||
#define TAG_CURRENT_GAME @"currentgame"
|
||||
|
||||
// Sunshine extension
|
||||
#define TAG_EXTERNAL_PORT @"ExternalPort"
|
||||
|
||||
@interface ServerInfoResponse : HttpResponse <Response>
|
||||
|
||||
- (void) populateWithData:(NSData *)data;
|
||||
|
||||
@@ -23,58 +23,17 @@
|
||||
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
|
||||
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] trim];
|
||||
|
||||
NSInteger httpsPort;
|
||||
if ([self getIntTag:TAG_HTTPS_PORT value:&httpsPort]) {
|
||||
host.httpsPort = (unsigned short)httpsPort;
|
||||
}
|
||||
else {
|
||||
// Use the default if it's not specified
|
||||
host.httpsPort = 47984;
|
||||
}
|
||||
|
||||
// We might get an IPv4 loopback address if we're using GS IPv6 Forwarder
|
||||
NSString *lanAddr = [[self getStringTag:TAG_LOCAL_IP] trim];
|
||||
if (![lanAddr hasPrefix:@"127."]) {
|
||||
unsigned short localPort;
|
||||
|
||||
// If we reached this host through this port, store our port there
|
||||
if (host.activeAddress && [lanAddr isEqualToString:[Utils addressPortStringToAddress:host.activeAddress]]) {
|
||||
localPort = [Utils addressPortStringToPort:host.activeAddress];
|
||||
}
|
||||
else if (host.localAddress) {
|
||||
// If there's an existing local address, use the port from that
|
||||
localPort = [Utils addressPortStringToPort:host.localAddress];
|
||||
}
|
||||
else {
|
||||
// If all else fails, use 47989
|
||||
localPort = 47989;
|
||||
}
|
||||
|
||||
host.localAddress = [Utils addressAndPortToAddressPortString:lanAddr port:localPort];
|
||||
}
|
||||
|
||||
// This is a Sunshine extension for WAN port remapping
|
||||
NSInteger externalHttpPort;
|
||||
if (![self getIntTag:TAG_EXTERNAL_PORT value:&externalHttpPort]) {
|
||||
// Use our active port if it's not specified
|
||||
if (host.activeAddress) {
|
||||
externalHttpPort = [Utils addressPortStringToPort:host.activeAddress];
|
||||
}
|
||||
else {
|
||||
// Otherwise use the default
|
||||
externalHttpPort = 47989;
|
||||
}
|
||||
host.localAddress = lanAddr;
|
||||
}
|
||||
|
||||
// Modern GFE versions don't actually give us a WAN address anymore
|
||||
// so we leave the one that we populated from mDNS discovery via STUN.
|
||||
NSString *wanAddr = [[self getStringTag:TAG_EXTERNAL_IP] trim];
|
||||
if (wanAddr) {
|
||||
host.externalAddress = [Utils addressAndPortToAddressPortString:wanAddr port:externalHttpPort];
|
||||
}
|
||||
else if (host.externalAddress) {
|
||||
// If we have an external address (via STUN) already, we still need to populate the port
|
||||
host.externalAddress = [Utils addressAndPortToAddressPortString:[Utils addressPortStringToAddress:host.externalAddress] port:externalHttpPort];
|
||||
host.externalAddress = wanAddr;
|
||||
}
|
||||
|
||||
NSString *state = [[self getStringTag:TAG_STATE] trim];
|
||||
@@ -85,9 +44,6 @@
|
||||
host.currentGame = @"0";
|
||||
}
|
||||
|
||||
// GFE uses the Mjolnir codename in their state enum values
|
||||
host.isNvidiaServerSoftware = [state containsString:@"MJOLNIR"];
|
||||
|
||||
NSInteger pairStatus;
|
||||
if ([self getIntTag:TAG_PAIR_STATUS value:&pairStatus]) {
|
||||
host.pairState = pairStatus ? PairStatePaired : PairStateUnpaired;
|
||||
|
||||
@@ -16,15 +16,12 @@
|
||||
|
||||
@implementation WakeOnLanManager
|
||||
|
||||
static const int numStaticPorts = 2;
|
||||
static const int staticPorts[numStaticPorts] = {
|
||||
static const int numPorts = 7;
|
||||
static const int ports[numPorts] = {
|
||||
9, // Standard WOL port (privileged port)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
static const int numDynamicPorts = 5;
|
||||
static const int dynamicPorts[numDynamicPorts] = {
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE/Sunshine
|
||||
};
|
||||
|
||||
+ (void) populateAddress:(struct sockaddr_storage*)addr withPort:(unsigned short)port {
|
||||
if (addr->ss_family == AF_INET) {
|
||||
@@ -41,33 +38,29 @@ static const int dynamicPorts[numDynamicPorts] = {
|
||||
NSData* wolPayload = [WakeOnLanManager createPayload:host];
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
NSString* address;
|
||||
const char* address;
|
||||
struct addrinfo hints, *res, *curr;
|
||||
|
||||
// try all ip addresses
|
||||
if (i == 0 && host.localAddress != nil) {
|
||||
address = host.localAddress;
|
||||
address = [host.localAddress UTF8String];
|
||||
} else if (i == 1 && host.externalAddress != nil) {
|
||||
address = host.externalAddress;
|
||||
address = [host.externalAddress UTF8String];
|
||||
} else if (i == 2 && host.address != nil) {
|
||||
address = host.address;
|
||||
address = [host.address UTF8String];
|
||||
} else if (i == 3 && host.ipv6Address != nil) {
|
||||
address = host.ipv6Address;
|
||||
address = [host.ipv6Address UTF8String];
|
||||
} else if (i == 4) {
|
||||
address = @"255.255.255.255";
|
||||
address = "255.255.255.255";
|
||||
} else {
|
||||
// Requested address wasn't present
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the raw address and base port from the address+port string
|
||||
NSString* rawAddress = [Utils addressPortStringToAddress:address];
|
||||
unsigned short basePort = [Utils addressPortStringToPort:address];
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_flags = AI_ADDRCONFIG;
|
||||
if (getaddrinfo([rawAddress UTF8String], NULL, &hints, &res) != 0 || res == NULL) {
|
||||
if (getaddrinfo(address, NULL, &hints, &res) != 0 || res == NULL) {
|
||||
// Failed to resolve address
|
||||
Log(LOG_E, @"Failed to resolve WOL address");
|
||||
continue;
|
||||
@@ -93,29 +86,15 @@ static const int dynamicPorts[numDynamicPorts] = {
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
|
||||
|
||||
for (int j = 0; j < numStaticPorts; j++) {
|
||||
[WakeOnLanManager populateAddress:&addr withPort:staticPorts[j]];
|
||||
for (int j = 0; j < numPorts; j++) {
|
||||
[WakeOnLanManager populateAddress:&addr withPort:ports[j]];
|
||||
long err = sendto(wolSocket,
|
||||
[wolPayload bytes],
|
||||
[wolPayload length],
|
||||
0,
|
||||
(struct sockaddr*)&addr,
|
||||
curr->ai_addrlen);
|
||||
Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", staticPorts[j], err);
|
||||
}
|
||||
|
||||
for (int j = 0; j < numDynamicPorts; j++) {
|
||||
// Offset the WoL dynamic ports by the base port
|
||||
unsigned short port = ((int)dynamicPorts[j] - 47989) + basePort;
|
||||
|
||||
[WakeOnLanManager populateAddress:&addr withPort:port];
|
||||
long err = sendto(wolSocket,
|
||||
[wolPayload bytes],
|
||||
[wolPayload length],
|
||||
0,
|
||||
(struct sockaddr*)&addr,
|
||||
curr->ai_addrlen);
|
||||
Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", port, err);
|
||||
Log(LOG_I, @"Sending WOL packet returned: %ld", err);
|
||||
}
|
||||
|
||||
close(wolSocket);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// OSPortabilityDefs.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 3/26/18.
|
||||
// Copyright © 2018 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef OSPortabilityDefs_h
|
||||
#define OSPortabilityDefs_h
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#define OSImage UIImage
|
||||
#define OSColor UIColor
|
||||
#define OSView UIView
|
||||
#define OSApplication UIApplication
|
||||
#else
|
||||
#define OSImage NSImage
|
||||
#define OSColor NSColor
|
||||
#define OSView NSView
|
||||
#define OSApplication NSApplication
|
||||
#endif
|
||||
|
||||
#endif /* OSPortabilityDefs_h */
|
||||
@@ -11,24 +11,23 @@
|
||||
|
||||
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
|
||||
|
||||
typedef struct {
|
||||
CFTimeInterval startTime;
|
||||
CFTimeInterval endTime;
|
||||
int totalFrames;
|
||||
int receivedFrames;
|
||||
int networkDroppedFrames;
|
||||
int totalHostProcessingLatency;
|
||||
int framesWithHostProcessingLatency;
|
||||
int maxHostProcessingLatency;
|
||||
int minHostProcessingLatency;
|
||||
} video_stats_t;
|
||||
@protocol ConnectionCallbacks <NSObject>
|
||||
|
||||
- (void) connectionStarted;
|
||||
- (void) connectionTerminated:(int)errorCode;
|
||||
- (void) stageStarting:(const char*)stageName;
|
||||
- (void) stageComplete:(const char*)stageName;
|
||||
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags;
|
||||
- (void) launchFailed:(NSString*)message;
|
||||
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
||||
- (void) connectionStatusUpdate:(int)status;
|
||||
|
||||
@end
|
||||
|
||||
@interface Connection : NSOperation <NSStreamDelegate>
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
|
||||
-(void) terminate;
|
||||
-(void) main;
|
||||
-(BOOL) getVideoStats:(video_stats_t*)stats;
|
||||
-(NSString*) getActiveCodecName;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
#import "Connection.h"
|
||||
#import "Utils.h"
|
||||
|
||||
#import <AudioUnit/AudioUnit.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
|
||||
#define SDL_MAIN_HANDLED
|
||||
#import <SDL.h>
|
||||
|
||||
#include "Limelight.h"
|
||||
#include "opus_multistream.h"
|
||||
|
||||
@@ -26,107 +25,41 @@
|
||||
char _hostString[256];
|
||||
char _appVersionString[32];
|
||||
char _gfeVersionString[32];
|
||||
char _rtspSessionUrl[128];
|
||||
}
|
||||
|
||||
static NSLock* initLock;
|
||||
static OpusMSDecoder* opusDecoder;
|
||||
static id<ConnectionCallbacks> _callbacks;
|
||||
static int lastFrameNumber;
|
||||
static int activeVideoFormat;
|
||||
static video_stats_t currentVideoStats;
|
||||
static video_stats_t lastVideoStats;
|
||||
static NSLock* videoStatsLock;
|
||||
|
||||
static SDL_AudioDeviceID audioDevice;
|
||||
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
|
||||
static void* audioBuffer;
|
||||
static int audioFrameSize;
|
||||
#define OUTPUT_BUS 0
|
||||
|
||||
// My iPod touch 5th Generation seems to really require 80 ms
|
||||
// of buffering to deliver glitch-free playback :(
|
||||
// FIXME: Maybe we can use a smaller buffer on more modern iOS versions?
|
||||
#define CIRCULAR_BUFFER_DURATION 80
|
||||
|
||||
static int audioBufferEntries;
|
||||
static int audioBufferWriteIndex;
|
||||
static int audioBufferReadIndex;
|
||||
static int audioBufferStride;
|
||||
static int audioSamplesPerFrame;
|
||||
static short* audioCircularBuffer;
|
||||
|
||||
#define AUDIO_QUEUE_BUFFERS 3
|
||||
|
||||
static AudioQueueRef audioQueue;
|
||||
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
|
||||
static VideoDecoderRenderer* renderer;
|
||||
|
||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
||||
{
|
||||
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
|
||||
lastFrameNumber = 0;
|
||||
activeVideoFormat = videoFormat;
|
||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
||||
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
|
||||
[renderer setupWithVideoFormat:videoFormat refreshRate:redrawRate];
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DrStart(void)
|
||||
void DrCleanup(void)
|
||||
{
|
||||
[renderer start];
|
||||
}
|
||||
|
||||
void DrStop(void)
|
||||
{
|
||||
[renderer stop];
|
||||
}
|
||||
|
||||
-(BOOL) getVideoStats:(video_stats_t*)stats
|
||||
{
|
||||
// We return lastVideoStats because it is a complete 1 second window
|
||||
[videoStatsLock lock];
|
||||
if (lastVideoStats.endTime != 0) {
|
||||
memcpy(stats, &lastVideoStats, sizeof(*stats));
|
||||
[videoStatsLock unlock];
|
||||
return YES;
|
||||
}
|
||||
|
||||
// No stats yet
|
||||
[videoStatsLock unlock];
|
||||
return NO;
|
||||
}
|
||||
|
||||
-(NSString*) getActiveCodecName
|
||||
{
|
||||
switch (activeVideoFormat)
|
||||
{
|
||||
case VIDEO_FORMAT_H264:
|
||||
return @"H.264";
|
||||
case VIDEO_FORMAT_H264_HIGH8_444:
|
||||
return @"H.264 (4:4:4)";
|
||||
case VIDEO_FORMAT_H265:
|
||||
return @"HEVC";
|
||||
case VIDEO_FORMAT_H265_REXT8_444:
|
||||
return @"HEVC (4:4:4)";
|
||||
case VIDEO_FORMAT_H265_MAIN10:
|
||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
||||
return @"HEVC Main 10 HDR";
|
||||
}
|
||||
else {
|
||||
return @"HEVC Main 10 SDR";
|
||||
}
|
||||
case VIDEO_FORMAT_H265_REXT10_444:
|
||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
||||
return @"HEVC Main 10 HDR (4:4:4)";
|
||||
}
|
||||
else {
|
||||
return @"HEVC Main 10 SDR (4:4:4)";
|
||||
}
|
||||
case VIDEO_FORMAT_AV1_MAIN8:
|
||||
return @"AV1";
|
||||
case VIDEO_FORMAT_AV1_HIGH8_444:
|
||||
return @"AV1 (4:4:4)";
|
||||
case VIDEO_FORMAT_AV1_MAIN10:
|
||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
||||
return @"AV1 10-bit HDR";
|
||||
}
|
||||
else {
|
||||
return @"AV1 10-bit SDR";
|
||||
}
|
||||
case VIDEO_FORMAT_AV1_HIGH10_444:
|
||||
if (LiGetCurrentHostDisplayHdrMode()) {
|
||||
return @"AV1 10-bit HDR (4:4:4)";
|
||||
}
|
||||
else {
|
||||
return @"AV1 10-bit SDR (4:4:4)";
|
||||
}
|
||||
default:
|
||||
return @"UNKNOWN";
|
||||
}
|
||||
[renderer cleanup];
|
||||
}
|
||||
|
||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
@@ -138,46 +71,6 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
// A frame was lost due to OOM condition
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
CFTimeInterval now = CACurrentMediaTime();
|
||||
if (!lastFrameNumber) {
|
||||
currentVideoStats.startTime = now;
|
||||
lastFrameNumber = decodeUnit->frameNumber;
|
||||
}
|
||||
else {
|
||||
// Flip stats roughly every second
|
||||
if (now - currentVideoStats.startTime >= 1.0f) {
|
||||
currentVideoStats.endTime = now;
|
||||
|
||||
[videoStatsLock lock];
|
||||
lastVideoStats = currentVideoStats;
|
||||
[videoStatsLock unlock];
|
||||
|
||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
||||
currentVideoStats.startTime = now;
|
||||
}
|
||||
|
||||
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
|
||||
currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
|
||||
currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
|
||||
lastFrameNumber = decodeUnit->frameNumber;
|
||||
}
|
||||
|
||||
if (decodeUnit->frameHostProcessingLatency != 0) {
|
||||
if (currentVideoStats.minHostProcessingLatency == 0 || decodeUnit->frameHostProcessingLatency < currentVideoStats.minHostProcessingLatency) {
|
||||
currentVideoStats.minHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
|
||||
}
|
||||
|
||||
if (decodeUnit->frameHostProcessingLatency > currentVideoStats.maxHostProcessingLatency) {
|
||||
currentVideoStats.maxHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
|
||||
}
|
||||
|
||||
currentVideoStats.framesWithHostProcessingLatency++;
|
||||
currentVideoStats.totalHostProcessingLatency += decodeUnit->frameHostProcessingLatency;
|
||||
}
|
||||
|
||||
currentVideoStats.receivedFrames++;
|
||||
currentVideoStats.totalFrames++;
|
||||
|
||||
PLENTRY entry = decodeUnit->bufferList;
|
||||
while (entry != NULL) {
|
||||
@@ -186,7 +79,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
|
||||
length:entry->length
|
||||
bufferType:entry->bufferType
|
||||
decodeUnit:decodeUnit];
|
||||
pts:decodeUnit->presentationTimeMs];
|
||||
if (ret != DR_OK) {
|
||||
free(data);
|
||||
return ret;
|
||||
@@ -204,60 +97,116 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
return [renderer submitDecodeBuffer:data
|
||||
length:offset
|
||||
bufferType:BUFFER_TYPE_PICDATA
|
||||
decodeUnit:decodeUnit];
|
||||
pts:decodeUnit->presentationTimeMs];
|
||||
}
|
||||
|
||||
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
|
||||
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION originalOpusConfig, void* context, int flags)
|
||||
{
|
||||
int err;
|
||||
SDL_AudioSpec want, have;
|
||||
AudioChannelLayout channelLayout = {};
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = *originalOpusConfig;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
|
||||
Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
SDL_zero(want);
|
||||
want.freq = opusConfig->sampleRate;
|
||||
want.format = AUDIO_S16;
|
||||
want.channels = opusConfig->channelCount;
|
||||
want.samples = opusConfig->samplesPerFrame;
|
||||
|
||||
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||
if (audioDevice == 0) {
|
||||
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
|
||||
ArCleanup();
|
||||
// Initialize the circular buffer
|
||||
audioBufferWriteIndex = audioBufferReadIndex = 0;
|
||||
audioSamplesPerFrame = opusConfig.samplesPerFrame;
|
||||
audioBufferStride = opusConfig.channelCount * opusConfig.samplesPerFrame;
|
||||
audioBufferEntries = CIRCULAR_BUFFER_DURATION / (opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000));
|
||||
audioCircularBuffer = malloc(audioBufferEntries * audioBufferStride * sizeof(short));
|
||||
if (audioCircularBuffer == NULL) {
|
||||
Log(LOG_E, @"Error allocating output queue\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
audioConfig = *opusConfig;
|
||||
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||
audioBuffer = SDL_malloc(audioFrameSize);
|
||||
if (audioBuffer == NULL) {
|
||||
Log(LOG_E, @"Failed to allocate audio frame buffer");
|
||||
ArCleanup();
|
||||
return -1;
|
||||
switch (opusConfig.channelCount) {
|
||||
case 2:
|
||||
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
|
||||
break;
|
||||
case 4:
|
||||
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic;
|
||||
break;
|
||||
case 6:
|
||||
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_5_1;
|
||||
break;
|
||||
case 8:
|
||||
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_7_1;
|
||||
|
||||
// Swap SL/SR and RL/RR to match the selected channel layout
|
||||
opusConfig.mapping[4] = originalOpusConfig->mapping[6];
|
||||
opusConfig.mapping[5] = originalOpusConfig->mapping[7];
|
||||
opusConfig.mapping[6] = originalOpusConfig->mapping[4];
|
||||
opusConfig.mapping[7] = originalOpusConfig->mapping[5];
|
||||
break;
|
||||
default:
|
||||
// Unsupported channel layout
|
||||
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig.channelCount);
|
||||
abort();
|
||||
}
|
||||
|
||||
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||
opusConfig->channelCount,
|
||||
opusConfig->streams,
|
||||
opusConfig->coupledStreams,
|
||||
opusConfig->mapping,
|
||||
opusDecoder = opus_multistream_decoder_create(opusConfig.sampleRate,
|
||||
opusConfig.channelCount,
|
||||
opusConfig.streams,
|
||||
opusConfig.coupledStreams,
|
||||
opusConfig.mapping,
|
||||
&err);
|
||||
if (opusDecoder == NULL) {
|
||||
Log(LOG_E, @"Failed to create Opus decoder");
|
||||
ArCleanup();
|
||||
return -1;
|
||||
|
||||
// Configure the audio session for our app
|
||||
NSError *audioSessionError = nil;
|
||||
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
||||
|
||||
[audioSession setPreferredSampleRate:opusConfig.sampleRate error:&audioSessionError];
|
||||
[audioSession setCategory:AVAudioSessionCategoryPlayback
|
||||
withOptions:AVAudioSessionCategoryOptionMixWithOthers
|
||||
error:&audioSessionError];
|
||||
[audioSession setPreferredIOBufferDuration:(opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000)) / 1000.0
|
||||
error:&audioSessionError];
|
||||
[audioSession setActive: YES error: &audioSessionError];
|
||||
|
||||
// FIXME: Calling this breaks surround audio for some reason
|
||||
//[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
|
||||
|
||||
OSStatus status;
|
||||
|
||||
AudioStreamBasicDescription audioFormat = {0};
|
||||
audioFormat.mSampleRate = opusConfig.sampleRate;
|
||||
audioFormat.mBitsPerChannel = 16;
|
||||
audioFormat.mFormatID = kAudioFormatLinearPCM;
|
||||
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
|
||||
audioFormat.mChannelsPerFrame = opusConfig.channelCount;
|
||||
audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * (audioFormat.mBitsPerChannel / 8);
|
||||
audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame;
|
||||
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
|
||||
audioFormat.mReserved = 0;
|
||||
|
||||
status = AudioQueueNewOutput(&audioFormat, FillOutputBuffer, nil, nil, nil, 0, &audioQueue);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Error allocating output queue: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Start playback
|
||||
SDL_PauseAudioDevice(audioDevice, 0);
|
||||
// We need to specify a channel layout for surround sound configurations
|
||||
status = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, sizeof(channelLayout));
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Error configuring surround channel layout: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Disable lowering volume of other audio streams (SDL sets AVAudioSessionCategoryOptionDuckOthers by default)
|
||||
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
|
||||
for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
|
||||
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * opusConfig.samplesPerFrame, &audioBuffers[i]);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
FillOutputBuffer(nil, audioQueue, audioBuffers[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
status = AudioQueueStart(audioQueue, nil);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Error starting queue: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void ArCleanup(void)
|
||||
@@ -267,43 +216,43 @@ void ArCleanup(void)
|
||||
opusDecoder = NULL;
|
||||
}
|
||||
|
||||
if (audioDevice != 0) {
|
||||
SDL_CloseAudioDevice(audioDevice);
|
||||
audioDevice = 0;
|
||||
// Stop before disposing to avoid massive delay inside
|
||||
// AudioQueueDispose() (iOS bug?)
|
||||
AudioQueueStop(audioQueue, true);
|
||||
|
||||
// Also frees buffers
|
||||
AudioQueueDispose(audioQueue, true);
|
||||
|
||||
// Must be freed after the queue is stopped
|
||||
if (audioCircularBuffer != NULL) {
|
||||
free(audioCircularBuffer);
|
||||
audioCircularBuffer = NULL;
|
||||
}
|
||||
|
||||
if (audioBuffer != NULL) {
|
||||
SDL_free(audioBuffer);
|
||||
audioBuffer = NULL;
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
// Audio session is now inactive
|
||||
[[AVAudioSession sharedInstance] setActive: NO error: nil];
|
||||
}
|
||||
|
||||
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
{
|
||||
int decodeLen;
|
||||
|
||||
// Don't queue if there's already more than 30 ms of audio data waiting
|
||||
// in Moonlight's audio queue.
|
||||
if (LiGetPendingAudioDuration() > 30) {
|
||||
// Check if there is space for this sample in the buffer. Again, this can race
|
||||
// but in the worst case, we'll not see the sample callback having consumed a sample.
|
||||
if (((audioBufferWriteIndex + 1) % audioBufferEntries) == audioBufferReadIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
|
||||
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
|
||||
(short*)&audioCircularBuffer[audioBufferWriteIndex * audioBufferStride], audioSamplesPerFrame, 0);
|
||||
if (decodeLen > 0) {
|
||||
// Provide backpressure on the queue to ensure too many frames don't build up
|
||||
// in SDL's audio queue.
|
||||
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
|
||||
SDL_Delay(1);
|
||||
}
|
||||
// Use a full memory barrier to ensure the circular buffer is written before incrementing the index
|
||||
__sync_synchronize();
|
||||
|
||||
if (SDL_QueueAudio(audioDevice,
|
||||
audioBuffer,
|
||||
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
|
||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
||||
}
|
||||
// This can race with the reader in the sample callback, however this is a benign
|
||||
// race since we'll either read the original value of s_WriteIndex (which is safe,
|
||||
// we just won't consider this sample) or the new value of s_WriteIndex
|
||||
audioBufferWriteIndex = (audioBufferWriteIndex + 1) % audioBufferEntries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,27 +299,6 @@ void ClConnectionStatusUpdate(int status)
|
||||
[_callbacks connectionStatusUpdate:status];
|
||||
}
|
||||
|
||||
void ClSetHdrMode(bool enabled)
|
||||
{
|
||||
[renderer setHdrMode:enabled];
|
||||
[_callbacks setHdrMode:enabled];
|
||||
}
|
||||
|
||||
void ClRumbleTriggers(uint16_t controllerNumber, uint16_t leftTriggerMotor, uint16_t rightTriggerMotor)
|
||||
{
|
||||
[_callbacks rumbleTriggers:controllerNumber leftTrigger:leftTriggerMotor rightTrigger:rightTriggerMotor];
|
||||
}
|
||||
|
||||
void ClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
|
||||
{
|
||||
[_callbacks setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
|
||||
}
|
||||
|
||||
void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
[_callbacks setControllerLed:controllerNumber r:r g:g b:b];
|
||||
}
|
||||
|
||||
-(void) terminate
|
||||
{
|
||||
// Interrupt any action blocking LiStartConnection(). This is
|
||||
@@ -399,26 +327,16 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
initLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
if (videoStatsLock == nil) {
|
||||
videoStatsLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
NSString *rawAddress = [Utils addressPortStringToAddress:config.host];
|
||||
strncpy(_hostString,
|
||||
[rawAddress cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_hostString) - 1);
|
||||
[config.host cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_hostString));
|
||||
strncpy(_appVersionString,
|
||||
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_appVersionString) - 1);
|
||||
sizeof(_appVersionString));
|
||||
if (config.gfeVersion != nil) {
|
||||
strncpy(_gfeVersionString,
|
||||
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_gfeVersionString) - 1);
|
||||
}
|
||||
if (config.rtspSessionUrl != nil) {
|
||||
strncpy(_rtspSessionUrl,
|
||||
[config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_rtspSessionUrl) - 1);
|
||||
sizeof(_gfeVersionString));
|
||||
}
|
||||
|
||||
LiInitializeServerInformation(&_serverInfo);
|
||||
@@ -427,10 +345,6 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
if (config.gfeVersion != nil) {
|
||||
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
|
||||
}
|
||||
if (config.rtspSessionUrl != nil) {
|
||||
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
|
||||
}
|
||||
_serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
|
||||
|
||||
renderer = myRenderer;
|
||||
_callbacks = callbacks;
|
||||
@@ -440,13 +354,13 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
_streamConfig.height = config.height;
|
||||
_streamConfig.fps = config.frameRate;
|
||||
_streamConfig.bitrate = config.bitRate;
|
||||
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
|
||||
_streamConfig.enableHdr = config.enableHdr;
|
||||
_streamConfig.audioConfiguration = config.audioConfiguration;
|
||||
|
||||
// Since we require iOS 12 or above, we're guaranteed to be running
|
||||
// on a 64-bit device with ARMv8 crypto instructions, so we don't
|
||||
// need to check for that here.
|
||||
_streamConfig.encryptionFlags = ENCFLG_ALL;
|
||||
// Use some of the HEVC encoding efficiency improvements to
|
||||
// reduce bandwidth usage while still gaining some image
|
||||
// quality improvement.
|
||||
_streamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
if ([Utils isActiveNetworkVPN]) {
|
||||
// Force remote streaming mode when a VPN is connected
|
||||
@@ -458,6 +372,23 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
_streamConfig.packetSize = 1392;
|
||||
}
|
||||
|
||||
// HDR implies HEVC allowed
|
||||
if (config.enableHdr) {
|
||||
config.allowHevc = YES;
|
||||
}
|
||||
|
||||
// On iOS 11, we can use HEVC if the server supports encoding it
|
||||
// and this device has hardware decode for it (A9 and later).
|
||||
// Additionally, iPhone X had a bug which would cause video
|
||||
// to freeze after a few minutes with HEVC prior to iOS 11.3.
|
||||
// As a result, we will only use HEVC on iOS 11.3 or later.
|
||||
if (@available(iOS 11.3, tvOS 11.3, *)) {
|
||||
_streamConfig.supportsHevc = config.allowHevc && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
|
||||
}
|
||||
|
||||
// HEVC must be supported when HDR is enabled
|
||||
assert(!_streamConfig.enableHdr || _streamConfig.supportsHevc);
|
||||
|
||||
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
|
||||
memset(_streamConfig.remoteInputAesIv, 0, 16);
|
||||
@@ -466,17 +397,20 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
|
||||
LiInitializeVideoCallbacks(&_drCallbacks);
|
||||
_drCallbacks.setup = DrDecoderSetup;
|
||||
_drCallbacks.start = DrStart;
|
||||
_drCallbacks.stop = DrStop;
|
||||
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
|
||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
|
||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
|
||||
_drCallbacks.cleanup = DrCleanup;
|
||||
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
|
||||
|
||||
// RFI doesn't work properly with HEVC on iOS 11 with an iPhone SE (at least)
|
||||
// It doesnt work on macOS either, tested with Network Link Conditioner.
|
||||
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
|
||||
CAPABILITY_DIRECT_SUBMIT;
|
||||
|
||||
LiInitializeAudioCallbacks(&_arCallbacks);
|
||||
_arCallbacks.init = ArInit;
|
||||
_arCallbacks.cleanup = ArCleanup;
|
||||
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
||||
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
|
||||
CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
|
||||
LiInitializeConnectionCallbacks(&_clCallbacks);
|
||||
_clCallbacks.stageStarting = ClStageStarting;
|
||||
@@ -487,14 +421,39 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
|
||||
_clCallbacks.logMessage = ClLogMessage;
|
||||
_clCallbacks.rumble = ClRumble;
|
||||
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
|
||||
_clCallbacks.setHdrMode = ClSetHdrMode;
|
||||
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
|
||||
_clCallbacks.setMotionEventState = ClSetMotionEventState;
|
||||
_clCallbacks.setControllerLED = ClSetControllerLED;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
static void FillOutputBuffer(void *aqData,
|
||||
AudioQueueRef inAQ,
|
||||
AudioQueueBufferRef inBuffer) {
|
||||
inBuffer->mAudioDataByteSize = audioBufferStride * sizeof(short);
|
||||
|
||||
assert(inBuffer->mAudioDataByteSize == inBuffer->mAudioDataBytesCapacity);
|
||||
|
||||
// If the indexes aren't equal, we have a sample
|
||||
if (audioBufferWriteIndex != audioBufferReadIndex) {
|
||||
// Copy data to the audio buffer
|
||||
memcpy(inBuffer->mAudioData,
|
||||
&audioCircularBuffer[audioBufferReadIndex * audioBufferStride],
|
||||
inBuffer->mAudioDataByteSize);
|
||||
|
||||
// Use a full memory barrier to ensure the circular buffer is read before incrementing the index
|
||||
__sync_synchronize();
|
||||
|
||||
// This can race with the reader in the AudDecDecodeAndPlaySample function. This is
|
||||
// not a problem because at worst, it just won't see that we've consumed this sample yet.
|
||||
audioBufferReadIndex = (audioBufferReadIndex + 1) % audioBufferEntries;
|
||||
}
|
||||
else {
|
||||
// No data, so play silence
|
||||
memset(inBuffer->mAudioData, 0, inBuffer->mAudioDataByteSize);
|
||||
}
|
||||
|
||||
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||
}
|
||||
|
||||
-(void) main
|
||||
{
|
||||
[initLock lock];
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// ConnectionCallbacks.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 11/1/20.
|
||||
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
@protocol ConnectionCallbacks <NSObject>
|
||||
|
||||
- (void) connectionStarted;
|
||||
- (void) connectionTerminated:(int)errorCode;
|
||||
- (void) stageStarting:(const char*)stageName;
|
||||
- (void) stageComplete:(const char*)stageName;
|
||||
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags;
|
||||
- (void) launchFailed:(NSString*)message;
|
||||
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
||||
- (void) connectionStatusUpdate:(int)status;
|
||||
- (void) setHdrMode:(bool)enabled;
|
||||
- (void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
|
||||
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
|
||||
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
|
||||
- (void) videoContentShown;
|
||||
|
||||
@end
|
||||
@@ -9,27 +9,24 @@
|
||||
@interface StreamConfiguration : NSObject
|
||||
|
||||
@property NSString* host;
|
||||
@property unsigned short httpsPort;
|
||||
@property NSString* appVersion;
|
||||
@property NSString* gfeVersion;
|
||||
@property NSString* appID;
|
||||
@property NSString* appName;
|
||||
@property NSString* rtspSessionUrl;
|
||||
@property int serverCodecModeSupport;
|
||||
@property int width;
|
||||
@property int height;
|
||||
@property int frameRate;
|
||||
@property int bitRate;
|
||||
@property int riKeyId;
|
||||
@property BOOL streamingRemotely;
|
||||
@property NSData* riKey;
|
||||
@property int gamepadMask;
|
||||
@property BOOL optimizeGameSettings;
|
||||
@property BOOL playAudioOnPC;
|
||||
@property BOOL swapABXYButtons;
|
||||
@property int audioConfiguration;
|
||||
@property int supportedVideoFormats;
|
||||
@property BOOL enableHdr;
|
||||
@property BOOL multiController;
|
||||
@property BOOL useFramePacing;
|
||||
@property BOOL allowHevc;
|
||||
@property NSData* serverCert;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@implementation StreamConfiguration
|
||||
@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport;
|
||||
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioConfiguration, enableHdr, multiController, allowHevc, serverCert;
|
||||
@end
|
||||
|
||||
@@ -11,10 +11,8 @@
|
||||
|
||||
@interface StreamManager : NSOperation
|
||||
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
||||
|
||||
- (void) stopStream;
|
||||
|
||||
- (NSString*) getStatsOverlayText;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,17 +17,15 @@
|
||||
#import "HttpRequest.h"
|
||||
#import "IdManager.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation StreamManager {
|
||||
StreamConfiguration* _config;
|
||||
|
||||
UIView* _renderView;
|
||||
OSView* _renderView;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
Connection* _connection;
|
||||
}
|
||||
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
|
||||
self = [super init];
|
||||
_config = config;
|
||||
_renderView = view;
|
||||
@@ -39,8 +37,10 @@
|
||||
|
||||
- (void)main {
|
||||
[CryptoManager generateKeyPairUsingSSL];
|
||||
NSString* uniqueId = [IdManager getUniqueId];
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithAddress:_config.host httpsPort:_config.httpsPort
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
|
||||
uniqueId:uniqueId
|
||||
serverCert:_config.serverCert];
|
||||
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
@@ -65,13 +65,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Only perform this check on GFE (as indicated by MJOLNIR in state value)
|
||||
if ((_config.width > 4096 || _config.height > 4096) && [serverState containsString:@"MJOLNIR"]) {
|
||||
// Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K.
|
||||
// We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation.
|
||||
NSString* codecSupport = [serverInfoResp getStringTag:@"ServerCodecModeSupport"];
|
||||
if (codecSupport == nil || !([codecSupport intValue] & 0x200)) {
|
||||
[_callbacks launchFailed:@"Your host PC's GPU doesn't support streaming video resolutions over 4K."];
|
||||
// resumeApp and launchApp handle calling launchFailed
|
||||
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
|
||||
// App already running, resume it
|
||||
if (![self resumeApp:hMan]) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Start app
|
||||
if (![self launchApp:hMan]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -80,26 +82,9 @@
|
||||
_config.appVersion = appversion;
|
||||
_config.gfeVersion = gfeVersion;
|
||||
|
||||
// resumeApp and launchApp handle calling launchFailed
|
||||
NSString* sessionUrl;
|
||||
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
|
||||
// App already running, resume it
|
||||
if (![self resumeApp:hMan receiveSessionUrl:&sessionUrl]) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Start app
|
||||
if (![self launchApp:hMan receiveSessionUrl:&sessionUrl]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate RTSP session URL from launch/resume response
|
||||
_config.rtspSessionUrl = sessionUrl;
|
||||
|
||||
// Initializing the renderer must be done on the main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks streamAspectRatio:(float)self->_config.width / (float)self->_config.height useFramePacing:self->_config.useFramePacing];
|
||||
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView];
|
||||
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:self->_connection];
|
||||
@@ -111,9 +96,9 @@
|
||||
[_connection terminate];
|
||||
}
|
||||
|
||||
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
|
||||
- (BOOL) launchApp:(HttpManager*)hMan {
|
||||
HttpResponse* launchResp = [[HttpResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"launch" config:_config]]];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchRequest:_config]]];
|
||||
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
|
||||
if (![launchResp isStatusOk]) {
|
||||
[_callbacks launchFailed:launchResp.statusMessage];
|
||||
@@ -125,13 +110,12 @@
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*sessionUrl = [launchResp getStringTag:@"sessionUrl0"];
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
- (BOOL) resumeApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
|
||||
- (BOOL) resumeApp:(HttpManager*)hMan {
|
||||
HttpResponse* resumeResp = [[HttpResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"resume" config:_config]]];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newResumeRequest:_config]]];
|
||||
NSString* resume = [resumeResp getStringTag:@"resume"];
|
||||
if (![resumeResp isStatusOk]) {
|
||||
[_callbacks launchFailed:resumeResp.statusMessage];
|
||||
@@ -143,50 +127,7 @@
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
*sessionUrl = [resumeResp getStringTag:@"sessionUrl0"];
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
- (NSString*) getStatsOverlayText {
|
||||
video_stats_t stats;
|
||||
|
||||
if (!_connection) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![_connection getVideoStats:&stats]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint32_t rtt, variance;
|
||||
NSString* latencyString;
|
||||
if (LiGetEstimatedRttInfo(&rtt, &variance)) {
|
||||
latencyString = [NSString stringWithFormat:@"%u ms (variance: %u ms)", rtt, variance];
|
||||
}
|
||||
else {
|
||||
latencyString = @"N/A";
|
||||
}
|
||||
|
||||
NSString* hostProcessingString;
|
||||
if (stats.framesWithHostProcessingLatency != 0) {
|
||||
hostProcessingString = [NSString stringWithFormat:@"\nHost processing latency min/max/avg: %.1f/%.1f/%.1f ms",
|
||||
stats.minHostProcessingLatency / 10.f,
|
||||
stats.maxHostProcessingLatency / 10.f,
|
||||
(float)stats.totalHostProcessingLatency / stats.framesWithHostProcessingLatency / 10.f];
|
||||
}
|
||||
else {
|
||||
hostProcessingString = @"";
|
||||
}
|
||||
|
||||
float interval = stats.endTime - stats.startTime;
|
||||
return [NSString stringWithFormat:@"Video stream: %dx%d %.2f FPS (Codec: %@)\nFrames dropped by your network connection: %.2f%%\nAverage network latency: %@%@",
|
||||
_config.width,
|
||||
_config.height,
|
||||
stats.totalFrames / interval,
|
||||
[_connection getActiveCodecName],
|
||||
stats.networkDroppedFrames / interval,
|
||||
latencyString,
|
||||
hostProcessingString];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,19 +8,16 @@
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
#import "ConnectionCallbacks.h"
|
||||
|
||||
#include "Limelight.h"
|
||||
|
||||
@interface VideoDecoderRenderer : NSObject
|
||||
|
||||
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
|
||||
- (id)initWithView:(OSView*)view;
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
|
||||
- (void)start;
|
||||
- (void)stop;
|
||||
- (void)setHdrMode:(BOOL)enabled;
|
||||
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate;
|
||||
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du;
|
||||
- (void)cleanup;
|
||||
|
||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
|
||||
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType pts:(unsigned int)pts;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,32 +9,19 @@
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamView.h"
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavcodec/cbs.h>
|
||||
#include <libavcodec/cbs_av1.h>
|
||||
#include <libavformat/avio.h>
|
||||
#include <libavutil/mem.h>
|
||||
|
||||
// Private libavformat API for writing the AV1 Codec Configuration Box
|
||||
extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
int write_seq_header);
|
||||
#include "Limelight.h"
|
||||
|
||||
@implementation VideoDecoderRenderer {
|
||||
StreamView* _view;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
float _streamAspectRatio;
|
||||
|
||||
AVSampleBufferDisplayLayer* displayLayer;
|
||||
Boolean waitingForSps, waitingForPps, waitingForVps;
|
||||
int videoFormat;
|
||||
int frameRate;
|
||||
|
||||
NSMutableArray *parameterSetBuffers;
|
||||
NSData *masteringDisplayColorVolume;
|
||||
NSData *contentLightLevelInfo;
|
||||
NSData *spsData, *ppsData, *vpsData;
|
||||
CMVideoFormatDescriptionRef formatDesc;
|
||||
|
||||
CADisplayLink* _displayLink;
|
||||
BOOL framePacing;
|
||||
}
|
||||
|
||||
- (void)reinitializeDisplayLayer
|
||||
@@ -42,23 +29,12 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
CALayer *oldLayer = displayLayer;
|
||||
|
||||
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
|
||||
displayLayer.bounds = _view.bounds;
|
||||
displayLayer.backgroundColor = [OSColor blackColor].CGColor;
|
||||
|
||||
// Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio
|
||||
// of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that
|
||||
// respects the PAR encoded in the SPS which causes our computed video-relative
|
||||
// touch location to be wrong in StreamView if the aspect ratio of the host
|
||||
// desktop doesn't match the aspect ratio of the stream.
|
||||
CGSize videoSize;
|
||||
if (_view.bounds.size.width > _view.bounds.size.height * _streamAspectRatio) {
|
||||
videoSize = CGSizeMake(_view.bounds.size.height * _streamAspectRatio, _view.bounds.size.height);
|
||||
} else {
|
||||
videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio);
|
||||
}
|
||||
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
|
||||
displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);
|
||||
displayLayer.videoGravity = AVLayerVideoGravityResize;
|
||||
|
||||
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||
|
||||
// Hide the layer until we get an IDR frame. This ensures we
|
||||
// can see the loading progress label as the stream is starting.
|
||||
displayLayer.hidden = YES;
|
||||
@@ -71,442 +47,236 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
|
||||
[_view.layer addSublayer:displayLayer];
|
||||
}
|
||||
|
||||
// We need some parameter sets before we can properly start decoding frames
|
||||
waitingForSps = true;
|
||||
spsData = nil;
|
||||
waitingForPps = true;
|
||||
ppsData = nil;
|
||||
waitingForVps = true;
|
||||
vpsData = nil;
|
||||
|
||||
if (formatDesc != nil) {
|
||||
CFRelease(formatDesc);
|
||||
formatDesc = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing
|
||||
- (id)initWithView:(StreamView*)view
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_view = view;
|
||||
_callbacks = callbacks;
|
||||
_streamAspectRatio = aspectRatio;
|
||||
framePacing = useFramePacing;
|
||||
|
||||
parameterSetBuffers = [[NSMutableArray alloc] init];
|
||||
|
||||
[self reinitializeDisplayLayer];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate
|
||||
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate
|
||||
{
|
||||
self->videoFormat = videoFormat;
|
||||
self->frameRate = frameRate;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||
if (@available(iOS 15.0, tvOS 15.0, *)) {
|
||||
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->frameRate, self->frameRate, self->frameRate);
|
||||
|
||||
if (refreshRate > 60) {
|
||||
// HACK: We seem to just get 60 Hz screen updates even with a 120 FPS stream if
|
||||
// we don't set preferredFramesPerSecond somewhere. Since we're a UIKit view, we
|
||||
// have to use CADisplayLink for that. See https://github.com/moonlight-stream/moonlight-ios/issues/372
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||
if (@available(iOS 10.0, tvOS 10.0, *)) {
|
||||
_displayLink.preferredFramesPerSecond = refreshRate;
|
||||
}
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
else {
|
||||
_displayLink.preferredFramesPerSecond = self->frameRate;
|
||||
}
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
|
||||
// TODO: Refactor this
|
||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
|
||||
|
||||
- (void)displayLinkCallback:(CADisplayLink *)sender
|
||||
{
|
||||
VIDEO_FRAME_HANDLE handle;
|
||||
PDECODE_UNIT du;
|
||||
|
||||
while (LiPollNextVideoFrame(&handle, &du)) {
|
||||
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
||||
|
||||
if (framePacing) {
|
||||
// Calculate the actual display refresh rate
|
||||
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
|
||||
|
||||
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
|
||||
// Battery saver, accessibility settings, or device thermals can cause the actual
|
||||
// refresh rate of the display to drop below the physical maximum.
|
||||
if (displayRefreshRate >= frameRate * 0.9f) {
|
||||
// Keep one pending frame to smooth out gaps due to
|
||||
// network jitter at the cost of 1 frame of latency
|
||||
if (LiGetPendingVideoFrames() == 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No-op - rendering done in submitDecodeBuffer
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
- (void)cleanup
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
}
|
||||
|
||||
#define FRAME_START_PREFIX_SIZE 4
|
||||
#define NALU_START_PREFIX_SIZE 3
|
||||
#define NAL_LENGTH_PREFIX_SIZE 4
|
||||
|
||||
- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength
|
||||
- (Boolean)readyForPictureData
|
||||
{
|
||||
OSStatus status;
|
||||
size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer);
|
||||
|
||||
// Append a 4 byte buffer to the frame block for the length prefix
|
||||
status = CMBlockBufferAppendMemoryBlock(frameBuffer, NULL,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
kCFAllocatorDefault, NULL, 0,
|
||||
NAL_LENGTH_PREFIX_SIZE, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the length prefix to the new buffer
|
||||
const int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, frameBuffer,
|
||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach the data buffer to the frame buffer by reference
|
||||
status = CMBlockBufferAppendBufferReference(frameBuffer, dataBuffer, offset + NALU_START_PREFIX_SIZE, dataLength, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSData*)getAv1CodecConfigurationBox:(NSData*)frameData {
|
||||
AVIOContext* ioctx = NULL;
|
||||
int err;
|
||||
|
||||
err = avio_open_dyn_buf(&ioctx);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"avio_open_dyn_buf() failed: %d", err);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Submit the IDR frame to write the av1C blob
|
||||
err = ff_isom_write_av1c(ioctx, (uint8_t*)frameData.bytes, (int)frameData.length, 1);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"ff_isom_write_av1c() failed: %d", err);
|
||||
// Fall-through to close and free buffer
|
||||
}
|
||||
|
||||
// Close the dynbuf and get the underlying buffer back (which we must free)
|
||||
uint8_t* av1cBuf = NULL;
|
||||
int av1cBufLen = avio_close_dyn_buf(ioctx, &av1cBuf);
|
||||
|
||||
Log(LOG_I, @"av1C block is %d bytes", av1cBufLen);
|
||||
|
||||
// Only return data if ff_isom_write_av1c() was successful
|
||||
NSData* data = nil;
|
||||
if (err >= 0 && av1cBufLen > 0) {
|
||||
data = [NSData dataWithBytes:av1cBuf length:av1cBufLen];
|
||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
return !waitingForSps && !waitingForPps;
|
||||
}
|
||||
else {
|
||||
data = nil;
|
||||
// H.265 requires VPS in addition to SPS and PPS
|
||||
return !waitingForVps && !waitingForSps && !waitingForPps;
|
||||
}
|
||||
|
||||
av_free(av1cBuf);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Much of this logic comes from Chrome
|
||||
- (CMVideoFormatDescriptionRef)createAV1FormatDescriptionForIDRFrame:(NSData*)frameData {
|
||||
NSMutableDictionary* extensions = [[NSMutableDictionary alloc] init];
|
||||
- (Boolean)isNalReferencePicture:(unsigned char)nalType
|
||||
{
|
||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
return nalType == 0x65;
|
||||
}
|
||||
else {
|
||||
// HEVC has several types of reference NALU types
|
||||
switch (nalType) {
|
||||
case 0x20:
|
||||
case 0x22:
|
||||
case 0x24:
|
||||
case 0x26:
|
||||
case 0x28:
|
||||
case 0x2A:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CodedBitstreamContext* cbsCtx = NULL;
|
||||
int err = ff_cbs_init(&cbsCtx, AV_CODEC_ID_AV1, NULL);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"ff_cbs_init() failed: %d", err);
|
||||
return nil;
|
||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
|
||||
{
|
||||
OSStatus status;
|
||||
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
|
||||
|
||||
// If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
|
||||
// so it can handle freeing it when the block buffer is destroyed
|
||||
if (offset == 1) {
|
||||
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
||||
|
||||
// Pass the real buffer pointer directly (no offset)
|
||||
// This will give it to the block buffer to free when it's released.
|
||||
// All further calls to CMBlockBufferAppendMemoryBlock will do so
|
||||
// at an offset and will not be asking the buffer to be freed.
|
||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
|
||||
nalLength + 1, // Add 1 for the offset we decremented
|
||||
kCFAllocatorDefault,
|
||||
NULL, 0, nalLength + 1, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the length prefix to existing buffer
|
||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
|
||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Append a 4 byte buffer to this block for the length prefix
|
||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, NULL,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
kCFAllocatorDefault, NULL, 0,
|
||||
NAL_LENGTH_PREFIX_SIZE, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write the length prefix to the new buffer
|
||||
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
||||
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
|
||||
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
|
||||
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
|
||||
oldOffset, NAL_LENGTH_PREFIX_SIZE);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach the buffer by reference to the block buffer
|
||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, &data[offset+NALU_START_PREFIX_SIZE],
|
||||
dataLength,
|
||||
kCFAllocatorNull, // Don't deallocate data on free
|
||||
NULL, 0, dataLength, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AVPacket avPacket = {};
|
||||
avPacket.data = (uint8_t*)frameData.bytes;
|
||||
avPacket.size = (int)frameData.length;
|
||||
|
||||
// Read the sequence header OBU
|
||||
CodedBitstreamFragment cbsFrag = {};
|
||||
err = ff_cbs_read_packet(cbsCtx, &cbsFrag, &avPacket);
|
||||
if (err < 0) {
|
||||
Log(LOG_E, @"ff_cbs_read_packet() failed: %d", err);
|
||||
ff_cbs_close(&cbsCtx);
|
||||
return nil;
|
||||
}
|
||||
|
||||
#define SET_CFSTR_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (__bridge NSString*)(value)
|
||||
#define SET_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (value)
|
||||
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_FormatName, @"av01");
|
||||
|
||||
// We use the value for YUV without alpha, same as Chrome
|
||||
// https://developer.apple.com/library/archive/qa/qa1183/_index.html
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_Depth, @24);
|
||||
|
||||
CodedBitstreamAV1Context* bitstreamCtx = (CodedBitstreamAV1Context*)cbsCtx->priv_data;
|
||||
AV1RawSequenceHeader* seqHeader = bitstreamCtx->sequence_header;
|
||||
if (seqHeader == NULL) {
|
||||
Log(LOG_E, @"AV1 sequence header not found in IDR frame!");
|
||||
ff_cbs_fragment_free(&cbsFrag);
|
||||
ff_cbs_close(&cbsCtx);
|
||||
return nil;
|
||||
}
|
||||
|
||||
switch (seqHeader->color_config.color_primaries) {
|
||||
case 1: // CP_BT_709
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
||||
kCMFormatDescriptionColorPrimaries_ITU_R_709_2);
|
||||
break;
|
||||
|
||||
case 6: // CP_BT_601
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
||||
kCMFormatDescriptionColorPrimaries_SMPTE_C);
|
||||
break;
|
||||
|
||||
case 9: // CP_BT_2020
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
|
||||
kCMFormatDescriptionColorPrimaries_ITU_R_2020);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported color_primaries value: %d", seqHeader->color_config.color_primaries);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (seqHeader->color_config.transfer_characteristics) {
|
||||
case 1: // TC_BT_709
|
||||
case 6: // TC_BT_601
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_ITU_R_709_2);
|
||||
break;
|
||||
|
||||
case 7: // TC_SMPTE_240
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_SMPTE_240M_1995);
|
||||
break;
|
||||
|
||||
case 8: // TC_LINEAR
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_Linear);
|
||||
break;
|
||||
|
||||
case 14: // TC_BT_2020_10_BIT
|
||||
case 15: // TC_BT_2020_12_BIT
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_ITU_R_2020);
|
||||
break;
|
||||
|
||||
case 16: // TC_SMPTE_2084
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ);
|
||||
break;
|
||||
|
||||
case 17: // TC_HLG
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
|
||||
kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported transfer_characteristics value: %d", seqHeader->color_config.transfer_characteristics);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (seqHeader->color_config.matrix_coefficients) {
|
||||
case 1: // MC_BT_709
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2);
|
||||
break;
|
||||
|
||||
case 6: // MC_BT_601
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4);
|
||||
break;
|
||||
|
||||
case 7: // MC_SMPTE_240
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995);
|
||||
break;
|
||||
|
||||
case 9: // MC_BT_2020_NCL
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
|
||||
kCMFormatDescriptionYCbCrMatrix_ITU_R_2020);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported matrix_coefficients value: %d", seqHeader->color_config.matrix_coefficients);
|
||||
break;
|
||||
}
|
||||
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_FullRangeVideo, @(seqHeader->color_config.color_range == 1));
|
||||
|
||||
// Progressive content
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_FieldCount, @(1));
|
||||
|
||||
switch (seqHeader->color_config.chroma_sample_position) {
|
||||
case 1: // CSP_VERTICAL
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
|
||||
kCMFormatDescriptionChromaLocation_Left);
|
||||
break;
|
||||
|
||||
case 2: // CSP_COLOCATED
|
||||
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
|
||||
kCMFormatDescriptionChromaLocation_TopLeft);
|
||||
break;
|
||||
|
||||
default:
|
||||
Log(LOG_W, @"Unsupported chroma_sample_position value: %d", seqHeader->color_config.chroma_sample_position);
|
||||
break;
|
||||
}
|
||||
|
||||
if (contentLightLevelInfo) {
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_ContentLightLevelInfo, contentLightLevelInfo);
|
||||
}
|
||||
|
||||
if (masteringDisplayColorVolume) {
|
||||
SET_EXTENSION(kCMFormatDescriptionExtension_MasteringDisplayColorVolume, masteringDisplayColorVolume);
|
||||
}
|
||||
|
||||
// Referenced the VP9 code in Chrome that performs a similar function
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/mac/vt_config_util.mm;drc=977dc02c431b4979e34c7792bc3d646f649dacb4;l=155
|
||||
extensions[(__bridge NSString*)kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms] =
|
||||
@{
|
||||
@"av1C" : [self getAv1CodecConfigurationBox:frameData],
|
||||
};
|
||||
extensions[@"BitsPerComponent"] = @(bitstreamCtx->bit_depth);
|
||||
|
||||
#undef SET_EXTENSION
|
||||
#undef SET_CFSTR_EXTENSION
|
||||
|
||||
// AV1 doesn't have a special format description function like H.264 and HEVC have, so we just use the generic one
|
||||
CMVideoFormatDescriptionRef formatDesc = NULL;
|
||||
OSStatus status = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_AV1,
|
||||
bitstreamCtx->frame_width, bitstreamCtx->frame_height,
|
||||
(__bridge CFDictionaryRef)extensions,
|
||||
&formatDesc);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create AV1 format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
ff_cbs_fragment_free(&cbsFrag);
|
||||
ff_cbs_close(&cbsCtx);
|
||||
return formatDesc;
|
||||
}
|
||||
|
||||
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType pts:(unsigned int)pts
|
||||
{
|
||||
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
|
||||
OSStatus status;
|
||||
|
||||
// Construct a new format description object each time we receive an IDR frame
|
||||
if (du->frameType == FRAME_TYPE_IDR) {
|
||||
if (bufferType != BUFFER_TYPE_PICDATA) {
|
||||
if (bufferType == BUFFER_TYPE_VPS || bufferType == BUFFER_TYPE_SPS || bufferType == BUFFER_TYPE_PPS) {
|
||||
// Add new parameter set into the parameter set array
|
||||
int startLen = data[2] == 0x01 ? 3 : 4;
|
||||
[parameterSetBuffers addObject:[NSData dataWithBytes:&data[startLen] length:length - startLen]];
|
||||
}
|
||||
if (bufferType != BUFFER_TYPE_PICDATA) {
|
||||
if (bufferType == BUFFER_TYPE_VPS) {
|
||||
Log(LOG_I, @"Got VPS");
|
||||
vpsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
||||
waitingForVps = false;
|
||||
|
||||
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
|
||||
// We got a new VPS so wait for a new SPS to match it
|
||||
waitingForSps = true;
|
||||
}
|
||||
else if (bufferType == BUFFER_TYPE_SPS) {
|
||||
Log(LOG_I, @"Got SPS");
|
||||
spsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
||||
waitingForSps = false;
|
||||
|
||||
// No frame data to submit for these NALUs
|
||||
return DR_OK;
|
||||
// We got a new SPS so wait for a new PPS to match it
|
||||
waitingForPps = true;
|
||||
} else if (bufferType == BUFFER_TYPE_PPS) {
|
||||
Log(LOG_I, @"Got PPS");
|
||||
ppsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
||||
waitingForPps = false;
|
||||
}
|
||||
|
||||
// Create the new format description when we get the first picture data buffer of an IDR frame.
|
||||
// This is the only way we know that there is no more CSD for this frame.
|
||||
//
|
||||
// NB: This logic depends on the fact that we submit all picture data in one buffer!
|
||||
|
||||
// Free the old format description
|
||||
if (formatDesc != NULL) {
|
||||
CFRelease(formatDesc);
|
||||
formatDesc = NULL;
|
||||
// See if we've got all the parameter sets we need for our video format
|
||||
if ([self readyForPictureData]) {
|
||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
|
||||
const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
|
||||
|
||||
Log(LOG_I, @"Constructing new H264 format description");
|
||||
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
||||
2, /* count of parameter sets */
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
&formatDesc);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const uint8_t* const parameterSetPointers[] = { [vpsData bytes], [spsData bytes], [ppsData bytes] };
|
||||
const size_t parameterSetSizes[] = { [vpsData length], [spsData length], [ppsData length] };
|
||||
|
||||
Log(LOG_I, @"Constructing new HEVC format description");
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
||||
3, /* count of parameter sets */
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
nil,
|
||||
&formatDesc);
|
||||
} else {
|
||||
// This means Moonlight-common-c decided to give us an HEVC stream
|
||||
// even though we said we couldn't support it. All we can do is abort().
|
||||
abort();
|
||||
}
|
||||
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||
// Construct parameter set arrays for the format description
|
||||
size_t parameterSetCount = [parameterSetBuffers count];
|
||||
const uint8_t* parameterSetPointers[parameterSetCount];
|
||||
size_t parameterSetSizes[parameterSetCount];
|
||||
for (int i = 0; i < parameterSetCount; i++) {
|
||||
NSData* parameterSet = parameterSetBuffers[i];
|
||||
parameterSetPointers[i] = parameterSet.bytes;
|
||||
parameterSetSizes[i] = parameterSet.length;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Constructing new H264 format description");
|
||||
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
||||
parameterSetCount,
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
&formatDesc);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
// Free parameter set buffers after submission
|
||||
[parameterSetBuffers removeAllObjects];
|
||||
}
|
||||
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||
// Construct parameter set arrays for the format description
|
||||
size_t parameterSetCount = [parameterSetBuffers count];
|
||||
const uint8_t* parameterSetPointers[parameterSetCount];
|
||||
size_t parameterSetSizes[parameterSetCount];
|
||||
for (int i = 0; i < parameterSetCount; i++) {
|
||||
NSData* parameterSet = parameterSetBuffers[i];
|
||||
parameterSetPointers[i] = parameterSet.bytes;
|
||||
parameterSetSizes[i] = parameterSet.length;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Constructing new HEVC format description");
|
||||
|
||||
NSMutableDictionary* videoFormatParams = [[NSMutableDictionary alloc] init];
|
||||
|
||||
if (contentLightLevelInfo) {
|
||||
[videoFormatParams setObject:contentLightLevelInfo forKey:(__bridge NSString*)kCMFormatDescriptionExtension_ContentLightLevelInfo];
|
||||
}
|
||||
|
||||
if (masteringDisplayColorVolume) {
|
||||
[videoFormatParams setObject:masteringDisplayColorVolume forKey:(__bridge NSString*)kCMFormatDescriptionExtension_MasteringDisplayColorVolume];
|
||||
}
|
||||
|
||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
||||
parameterSetCount,
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
(__bridge CFDictionaryRef)videoFormatParams,
|
||||
&formatDesc);
|
||||
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
|
||||
// Free parameter set buffers after submission
|
||||
[parameterSetBuffers removeAllObjects];
|
||||
}
|
||||
else if (videoFormat & VIDEO_FORMAT_MASK_AV1) {
|
||||
NSData* fullFrameData = [NSData dataWithBytesNoCopy:data length:length freeWhenDone:NO];
|
||||
|
||||
Log(LOG_I, @"Constructing new AV1 format description");
|
||||
formatDesc = [self createAV1FormatDescriptionForIDRFrame:fullFrameData];
|
||||
}
|
||||
else {
|
||||
// Unsupported codec!
|
||||
abort();
|
||||
}
|
||||
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
|
||||
|
||||
// No frame data to submit for these NALUs
|
||||
return DR_OK;
|
||||
}
|
||||
|
||||
if (formatDesc == NULL) {
|
||||
@@ -519,9 +289,13 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
|
||||
|
||||
// Recreate the display layer. We are already on the main thread,
|
||||
// so this is safe to do right here.
|
||||
[self reinitializeDisplayLayer];
|
||||
// Recreate the display layer on the main thread.
|
||||
// We need to use dispatch_sync() or we may miss
|
||||
// some parameter sets while the layer is being
|
||||
// recreated.
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[self reinitializeDisplayLayer];
|
||||
});
|
||||
|
||||
// Request an IDR frame to initialize the new decoder
|
||||
free(data);
|
||||
@@ -529,156 +303,82 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
}
|
||||
|
||||
// Now we're decoding actual frame data here
|
||||
CMBlockBufferRef frameBlockBuffer;
|
||||
CMBlockBufferRef dataBlockBuffer;
|
||||
CMBlockBufferRef blockBuffer;
|
||||
|
||||
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorDefault, NULL, 0, length, 0, &dataBlockBuffer);
|
||||
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status);
|
||||
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
|
||||
free(data);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
int lastOffset = -1;
|
||||
for (int i = 0; i < length - FRAME_START_PREFIX_SIZE; i++) {
|
||||
// Search for a NALU
|
||||
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
||||
// It's the start of a new NALU
|
||||
if (lastOffset != -1) {
|
||||
// We've seen a start before this so enqueue that NALU
|
||||
[self updateBufferForRange:blockBuffer data:data offset:lastOffset length:i - lastOffset];
|
||||
}
|
||||
|
||||
lastOffset = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOffset != -1) {
|
||||
// Enqueue the remaining data
|
||||
[self updateBufferForRange:blockBuffer data:data offset:lastOffset length:length - lastOffset];
|
||||
}
|
||||
|
||||
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
|
||||
|
||||
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &frameBlockBuffer);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
|
||||
CFRelease(dataBlockBuffer);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// H.264 and HEVC formats require NAL prefix fixups from Annex B to length-delimited
|
||||
if (videoFormat & (VIDEO_FORMAT_MASK_H264 | VIDEO_FORMAT_MASK_H265)) {
|
||||
int lastOffset = -1;
|
||||
for (int i = 0; i < length - NALU_START_PREFIX_SIZE; i++) {
|
||||
// Search for a NALU
|
||||
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
||||
// It's the start of a new NALU
|
||||
if (lastOffset != -1) {
|
||||
// We've seen a start before this so enqueue that NALU
|
||||
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:i - lastOffset];
|
||||
}
|
||||
|
||||
lastOffset = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOffset != -1) {
|
||||
// Enqueue the remaining data
|
||||
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:length - lastOffset];
|
||||
}
|
||||
}
|
||||
else {
|
||||
// For formats that require no length-changing fixups, just append a reference to the raw data block
|
||||
status = CMBlockBufferAppendBufferReference(frameBlockBuffer, dataBlockBuffer, 0, length, 0);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
}
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
|
||||
CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeUs, 1000000), kCMTimeInvalid};
|
||||
|
||||
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
|
||||
frameBlockBuffer,
|
||||
formatDesc, 1, 1,
|
||||
&sampleTiming, 0, NULL,
|
||||
status = CMSampleBufferCreate(kCFAllocatorDefault,
|
||||
blockBuffer,
|
||||
true, NULL,
|
||||
NULL, formatDesc, 1, 0,
|
||||
NULL, 0, NULL,
|
||||
&sampleBuffer);
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
|
||||
CFRelease(dataBlockBuffer);
|
||||
CFRelease(frameBlockBuffer);
|
||||
CFRelease(blockBuffer);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
// Enqueue the next frame
|
||||
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
|
||||
if (du->frameType == FRAME_TYPE_IDR) {
|
||||
// Ensure the layer is visible now
|
||||
self->displayLayer.hidden = NO;
|
||||
|
||||
// Tell our parent VC to hide the progress indicator
|
||||
[self->_callbacks videoContentShown];
|
||||
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
|
||||
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
|
||||
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
|
||||
|
||||
if (![self isNalReferencePicture:nalType]) {
|
||||
// P-frame
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
|
||||
} else {
|
||||
// I-frame
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
|
||||
}
|
||||
|
||||
// Dereference the buffers
|
||||
CFRelease(dataBlockBuffer);
|
||||
CFRelease(frameBlockBuffer);
|
||||
CFRelease(sampleBuffer);
|
||||
|
||||
// Enqueue video samples on the main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Enqueue the next frame
|
||||
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
|
||||
if ([self isNalReferencePicture:nalType]) {
|
||||
// Ensure the layer is visible now
|
||||
self->displayLayer.hidden = NO;
|
||||
}
|
||||
|
||||
// Dereference the buffers
|
||||
CFRelease(blockBuffer);
|
||||
CFRelease(sampleBuffer);
|
||||
});
|
||||
|
||||
return DR_OK;
|
||||
}
|
||||
|
||||
- (void)setHdrMode:(BOOL)enabled {
|
||||
SS_HDR_METADATA hdrMetadata;
|
||||
|
||||
BOOL hasMetadata = enabled && LiGetHdrMetadata(&hdrMetadata);
|
||||
BOOL metadataChanged = NO;
|
||||
|
||||
if (hasMetadata && hdrMetadata.displayPrimaries[0].x != 0 && hdrMetadata.maxDisplayLuminance != 0) {
|
||||
// This data is all in big-endian
|
||||
struct {
|
||||
vector_ushort2 primaries[3];
|
||||
vector_ushort2 white_point;
|
||||
uint32_t luminance_max;
|
||||
uint32_t luminance_min;
|
||||
} __attribute__((packed, aligned(4))) mdcv;
|
||||
|
||||
// mdcv is in GBR order while SS_HDR_METADATA is in RGB order
|
||||
mdcv.primaries[0].x = __builtin_bswap16(hdrMetadata.displayPrimaries[1].x);
|
||||
mdcv.primaries[0].y = __builtin_bswap16(hdrMetadata.displayPrimaries[1].y);
|
||||
mdcv.primaries[1].x = __builtin_bswap16(hdrMetadata.displayPrimaries[2].x);
|
||||
mdcv.primaries[1].y = __builtin_bswap16(hdrMetadata.displayPrimaries[2].y);
|
||||
mdcv.primaries[2].x = __builtin_bswap16(hdrMetadata.displayPrimaries[0].x);
|
||||
mdcv.primaries[2].y = __builtin_bswap16(hdrMetadata.displayPrimaries[0].y);
|
||||
|
||||
mdcv.white_point.x = __builtin_bswap16(hdrMetadata.whitePoint.x);
|
||||
mdcv.white_point.y = __builtin_bswap16(hdrMetadata.whitePoint.y);
|
||||
|
||||
// These luminance values are in 10000ths of a nit
|
||||
mdcv.luminance_max = __builtin_bswap32((uint32_t)hdrMetadata.maxDisplayLuminance * 10000);
|
||||
mdcv.luminance_min = __builtin_bswap32(hdrMetadata.minDisplayLuminance);
|
||||
|
||||
NSData* newMdcv = [NSData dataWithBytes:&mdcv length:sizeof(mdcv)];
|
||||
if (masteringDisplayColorVolume == nil || ![newMdcv isEqualToData:masteringDisplayColorVolume]) {
|
||||
masteringDisplayColorVolume = newMdcv;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
}
|
||||
else if (masteringDisplayColorVolume != nil) {
|
||||
masteringDisplayColorVolume = nil;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
|
||||
if (hasMetadata && hdrMetadata.maxContentLightLevel != 0 && hdrMetadata.maxFrameAverageLightLevel != 0) {
|
||||
// This data is all in big-endian
|
||||
struct {
|
||||
uint16_t max_content_light_level;
|
||||
uint16_t max_frame_average_light_level;
|
||||
} __attribute__((packed, aligned(2))) cll;
|
||||
|
||||
cll.max_content_light_level = __builtin_bswap16(hdrMetadata.maxContentLightLevel);
|
||||
cll.max_frame_average_light_level = __builtin_bswap16(hdrMetadata.maxFrameAverageLightLevel);
|
||||
|
||||
NSData* newCll = [NSData dataWithBytes:&cll length:sizeof(cll)];
|
||||
if (contentLightLevelInfo == nil || ![newCll isEqualToData:contentLightLevelInfo]) {
|
||||
contentLightLevelInfo = newCll;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
}
|
||||
else if (contentLightLevelInfo != nil) {
|
||||
contentLightLevelInfo = nil;
|
||||
metadataChanged = YES;
|
||||
}
|
||||
|
||||
// If the metadata changed, request an IDR frame to re-create the CMVideoFormatDescription
|
||||
if (metadataChanged) {
|
||||
LiRequestIdrFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,16 +11,11 @@
|
||||
|
||||
@protocol AppCallback <NSObject>
|
||||
|
||||
- (void) appClicked:(TemporaryApp*)app view:(UIView*)view;
|
||||
- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
|
||||
- (void) appClicked:(TemporaryApp*) app;
|
||||
|
||||
@end
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface UIAppView : UIButton <UIContextMenuInteractionDelegate>
|
||||
#else
|
||||
@interface UIAppView : UIButton
|
||||
#endif
|
||||
|
||||
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
|
||||
- (void) updateAppImage;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#import "UIAppView.h"
|
||||
#import "AppAssetManager.h"
|
||||
|
||||
static const float REFRESH_CYCLE = 1.0f;
|
||||
static const float REFRESH_CYCLE = 2.0f;
|
||||
|
||||
@implementation UIAppView {
|
||||
TemporaryApp* _app;
|
||||
@@ -40,28 +40,17 @@ static UIImage* noImage;
|
||||
self.frame = CGRectMake(0, 0, 150, 200);
|
||||
#endif
|
||||
|
||||
[self setAlpha:app.hidden ? 0.4 : 1.0];
|
||||
|
||||
_appImage = [[UIImageView alloc] initWithFrame:self.frame];
|
||||
[_appImage setImage:noImage];
|
||||
[self addSubview:_appImage];
|
||||
|
||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
||||
// for tvOS devices and iOS prior to 13.0.
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
|
||||
[self addInteraction:rightClickInteraction];
|
||||
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||
[self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(appLongClicked:)];
|
||||
[self addGestureRecognizer:longPressRecognizer];
|
||||
else {
|
||||
[self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
[self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
|
||||
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
|
||||
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
||||
|
||||
@@ -80,40 +69,15 @@ static UIImage* noImage;
|
||||
#endif
|
||||
|
||||
[self updateAppImage];
|
||||
[self startUpdateLoop];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
// Start our update loop when we are added to our cell
|
||||
if (self.superview != nil) {
|
||||
[self updateLoop];
|
||||
}
|
||||
- (void) appClicked {
|
||||
[_callback appClicked:_app];
|
||||
}
|
||||
|
||||
- (void) appClicked:(UIView *)view {
|
||||
[_callback appClicked:_app view:view];
|
||||
}
|
||||
|
||||
- (void) appLongClicked:(UILongPressGestureRecognizer*)gesture {
|
||||
if (gesture.state == UIGestureRecognizerStateBegan) {
|
||||
[_callback appLongClicked:_app view:self];
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
|
||||
configurationForMenuAtLocation:(CGPoint)location {
|
||||
// We don't want to trigger the primary action at this point, so cancel
|
||||
// tracking touch on this view now. This will also have the (intended)
|
||||
// effect of removing the touch highlight on this view.
|
||||
[self cancelTrackingWithEvent:nil];
|
||||
|
||||
[_callback appLongClicked:_app view:self];
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void) updateAppImage {
|
||||
if (_appOverlay != nil) {
|
||||
[_appOverlay removeFromSuperview];
|
||||
@@ -210,28 +174,21 @@ static UIImage* noImage;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) startUpdateLoop {
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
}
|
||||
|
||||
- (void) updateLoop {
|
||||
// Stop immediately if the view has been detached
|
||||
if (self.superview == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the app image if neccessary
|
||||
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
|
||||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
|
||||
[self updateAppImage];
|
||||
}
|
||||
|
||||
// Show no shadow for hidden apps. Because we adjust the opacity of the
|
||||
// cells for hidden apps, it makes them look bad when the shadow draws
|
||||
// through the app tile.
|
||||
self.superview.layer.shadowOpacity = _app.hidden ? 0.0f : 0.5f;
|
||||
|
||||
// Update opacity if neccessary
|
||||
[self setAlpha:_app.hidden ? 0.4 : 1.0];
|
||||
|
||||
// Queue the next refresh cycle
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
// Stop updating when we detach from our parent view
|
||||
if (self.superview != nil) {
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
|
||||
@end
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@interface UIComputerView : UIButton <UIContextMenuInteractionDelegate>
|
||||
#else
|
||||
@interface UIComputerView : UIButton
|
||||
#endif
|
||||
|
||||
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback;
|
||||
- (id) initForAddWithCallback:(id<HostCallback>)callback;
|
||||
|
||||
@@ -102,7 +102,12 @@ static const int LABEL_DY = 20;
|
||||
self = [self init];
|
||||
_callback = callback;
|
||||
|
||||
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
}
|
||||
else {
|
||||
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
[_hostLabel setText:@"Add Host Manually"];
|
||||
[_hostLabel sizeToFit];
|
||||
@@ -119,34 +124,22 @@ static const int LABEL_DY = 20;
|
||||
_host = host;
|
||||
_callback = callback;
|
||||
|
||||
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
|
||||
// for tvOS devices and iOS prior to 13.0.
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
|
||||
[self addInteraction:rightClickInteraction];
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
|
||||
[self addGestureRecognizer:longPressRecognizer];
|
||||
}
|
||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
|
||||
[self addGestureRecognizer:longPressRecognizer];
|
||||
|
||||
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
}
|
||||
else {
|
||||
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
[self updateContentsForHost:host];
|
||||
[self startUpdateLoop];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
// Start our update loop when we are added to our cell
|
||||
if (self.superview != nil && _host != nil) {
|
||||
[self updateLoop];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) updateBounds {
|
||||
float x = FLT_MAX;
|
||||
float y = FLT_MAX;
|
||||
@@ -197,16 +190,17 @@ static const int LABEL_DY = 20;
|
||||
[self updateBounds];
|
||||
}
|
||||
|
||||
- (void) startUpdateLoop {
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
}
|
||||
|
||||
- (void) updateLoop {
|
||||
// Stop immediately if the view has been detached
|
||||
if (self.superview == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self updateContentsForHost:_host];
|
||||
|
||||
// Queue the next refresh cycle
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
// Stop updating when we detach from our parent view
|
||||
if (self.superview != nil) {
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
|
||||
@@ -215,19 +209,6 @@ static const int LABEL_DY = 20;
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
|
||||
configurationForMenuAtLocation:(CGPoint)location {
|
||||
// We don't want to trigger the primary action at this point, so cancel
|
||||
// tracking touch on this view now. This will also have the (intended)
|
||||
// effect of removing the touch highlight on this view.
|
||||
[self cancelTrackingWithEvent:nil];
|
||||
|
||||
[_callback hostLongClicked:_host view:self];
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void) hostClicked {
|
||||
[_callback hostClicked:_host view:self];
|
||||
}
|
||||
|
||||
@@ -27,14 +27,6 @@ FOUNDATION_EXPORT NSString *const deviceName;
|
||||
+ (NSData*) hexToBytes:(NSString*) hex;
|
||||
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
|
||||
+ (BOOL) isActiveNetworkVPN;
|
||||
+ (BOOL) parseAddressPortString:(NSString*)addressPort address:(NSRange*)address port:(NSRange*)port;
|
||||
+ (NSString*) addressPortStringToAddress:(NSString*)addressPort;
|
||||
+ (unsigned short) addressPortStringToPort:(NSString*)addressPort;
|
||||
+ (NSString*) addressAndPortToAddressPortString:(NSString*)address port:(unsigned short)port;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
+ (void) launchUrl:(NSString*)urlString;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -64,88 +64,15 @@ NSString *const deviceName = @"roth";
|
||||
return NO;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
+ (void) launchUrl:(NSString*)urlString {
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil];
|
||||
}
|
||||
#endif
|
||||
|
||||
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog {
|
||||
#if !TARGET_OS_TV
|
||||
// tvOS doesn't have a browser
|
||||
[dialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"];
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
|
||||
}]];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (BOOL) parseAddressPortString:(NSString*)addressPort address:(NSRange*)address port:(NSRange*)port {
|
||||
if (![addressPort containsString:@":"]) {
|
||||
// If there's no port or IPv6 separator, the whole thing is an address
|
||||
*address = NSMakeRange(0, [addressPort length]);
|
||||
*port = NSMakeRange(NSNotFound, 0);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
NSInteger locationOfOpeningBracket = [addressPort rangeOfString:@"["].location;
|
||||
NSInteger locationOfClosingBracket = [addressPort rangeOfString:@"]"].location;
|
||||
if (locationOfOpeningBracket != NSNotFound || locationOfClosingBracket != NSNotFound) {
|
||||
// If we have brackets, it's an IPv6 address
|
||||
if (locationOfOpeningBracket == NSNotFound || locationOfClosingBracket == NSNotFound ||
|
||||
locationOfClosingBracket < locationOfOpeningBracket) {
|
||||
// Invalid address format
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Cut at the brackets
|
||||
*address = NSMakeRange(locationOfOpeningBracket + 1, locationOfClosingBracket - locationOfOpeningBracket - 1);
|
||||
}
|
||||
else {
|
||||
// It's an IPv4 address, so just cut at the port separator
|
||||
*address = NSMakeRange(0, [addressPort rangeOfString:@":"].location);
|
||||
}
|
||||
|
||||
NSUInteger remainingStringLocation = address->location + address->length;
|
||||
NSRange remainingStringRange = NSMakeRange(remainingStringLocation, [addressPort length] - remainingStringLocation);
|
||||
NSInteger locationOfPortSeparator = [addressPort rangeOfString:@":" options:0 range:remainingStringRange].location;
|
||||
if (locationOfPortSeparator != NSNotFound) {
|
||||
*port = NSMakeRange(locationOfPortSeparator + 1, [addressPort length] - locationOfPortSeparator - 1);
|
||||
}
|
||||
else {
|
||||
*port = NSMakeRange(NSNotFound, 0);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
+ (NSString*) addressPortStringToAddress:(NSString*)addressPort {
|
||||
NSRange addressRange, portRange;
|
||||
if (![self parseAddressPortString:addressPort address:&addressRange port:&portRange]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [addressPort substringWithRange:addressRange];
|
||||
}
|
||||
|
||||
+ (unsigned short) addressPortStringToPort:(NSString*)addressPort {
|
||||
NSRange addressRange, portRange;
|
||||
if (![self parseAddressPortString:addressPort address:&addressRange port:&portRange] || portRange.location == NSNotFound) {
|
||||
return 47989;
|
||||
}
|
||||
|
||||
return [[addressPort substringWithRange:portRange] integerValue];
|
||||
}
|
||||
|
||||
+ (NSString*) addressAndPortToAddressPortString:(NSString*)address port:(unsigned short)port {
|
||||
if ([address containsString:@":"]) {
|
||||
// IPv6 addresses require escaping
|
||||
return [NSString stringWithFormat:@"[%@]:%u", address, port];
|
||||
}
|
||||
else {
|
||||
return [NSString stringWithFormat:@"%@:%u", address, port];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSString (NSStringWithTrim)
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
@implementation MainFrameViewController {
|
||||
NSOperationQueue* _opQueue;
|
||||
TemporaryHost* _selectedHost;
|
||||
BOOL _showHiddenApps;
|
||||
NSString* _uniqueId;
|
||||
NSData* _clientCert;
|
||||
DiscoveryManager* _discMan;
|
||||
@@ -64,7 +63,7 @@ static NSMutableSet* hostList;
|
||||
// failure callback could be invoked.
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
|
||||
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@\n\nIf your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.", PIN]
|
||||
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
|
||||
self->_pairAlert = nil;
|
||||
@@ -168,7 +167,7 @@ static NSMutableSet* hostList;
|
||||
// Exempt this host from discovery while handling the applist query
|
||||
[self->_discMan pauseDiscoveryForHost:host];
|
||||
|
||||
AppListResponse* appListResp = [ConnectionHelper getAppListForHost:host];
|
||||
AppListResponse* appListResp = [ConnectionHelper getAppListForHostWithHostIP:host.activeAddress serverCert:host.serverCert uniqueID:self->_uniqueId];
|
||||
|
||||
[self->_discMan resumeDiscoveryForHost:host];
|
||||
|
||||
@@ -209,24 +208,6 @@ static NSMutableSet* hostList;
|
||||
});
|
||||
}
|
||||
|
||||
- (void) updateAppEntry:(TemporaryApp*)app forHost:(TemporaryHost*)host {
|
||||
DataManager* database = [[DataManager alloc] init];
|
||||
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
|
||||
|
||||
for (TemporaryApp* savedApp in newHostAppList) {
|
||||
if ([app.id isEqualToString:savedApp.id]) {
|
||||
savedApp.name = app.name;
|
||||
savedApp.hdrSupported = app.hdrSupported;
|
||||
savedApp.hidden = app.hidden;
|
||||
|
||||
host.appList = newHostAppList;
|
||||
|
||||
[database updateAppsForExistingHost:host];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host {
|
||||
DataManager* database = [[DataManager alloc] init];
|
||||
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
|
||||
@@ -237,7 +218,6 @@ static NSMutableSet* hostList;
|
||||
if ([app.id isEqualToString:savedApp.id]) {
|
||||
savedApp.name = app.name;
|
||||
savedApp.hdrSupported = app.hdrSupported;
|
||||
// Don't propagate hidden, because we want the local data to prevail
|
||||
appAlreadyInList = YES;
|
||||
break;
|
||||
}
|
||||
@@ -295,7 +275,6 @@ static NSMutableSet* hostList;
|
||||
#endif
|
||||
|
||||
[_appManager stopRetrieving];
|
||||
_showHiddenApps = NO;
|
||||
_selectedHost = nil;
|
||||
_sortedAppList = nil;
|
||||
|
||||
@@ -373,7 +352,7 @@ static NSMutableSet* hostList;
|
||||
return;
|
||||
}
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:self->_uniqueId serverCert:host.serverCert];
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
|
||||
// Exempt this host from discovery while handling the serverinfo request
|
||||
@@ -487,20 +466,6 @@ static NSMutableSet* hostList;
|
||||
[[self activeViewController] presentViewController:wolAlert animated:YES completion:nil];
|
||||
}]];
|
||||
}
|
||||
else if (host.pairState == PairStatePaired) {
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"View All Apps" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
self->_showHiddenApps = YES;
|
||||
[self hostClicked:host view:view];
|
||||
}]];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
if (host.isNvidiaServerSoftware) {
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"NVIDIA GameStream End-of-Service" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ"];
|
||||
}]];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Test Network" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) {
|
||||
[self showLoadingFrame:^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
@@ -517,9 +482,16 @@ static NSMutableSet* hostList;
|
||||
message = @"The network test could not be performed because none of Moonlight's connection testing servers were reachable. Check your Internet connection or try again later.";
|
||||
}
|
||||
else {
|
||||
char blockedPorts[512];
|
||||
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
|
||||
message = [NSString stringWithFormat:@"Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n%s", blockedPorts];
|
||||
message = @"Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n";
|
||||
|
||||
for (int i = 0; i < 32; i++) {
|
||||
if (portTestResult & (1 << i)) {
|
||||
NSString* portString = [NSString stringWithFormat:@"%s %u\n",
|
||||
LiGetProtocolFromPortFlagIndex(i) == 17 ? "UDP" : "TCP",
|
||||
LiGetPortFromPortFlagIndex(i)];
|
||||
message = [message stringByAppendingString:portString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIAlertController* netTestAlert = [UIAlertController alertControllerWithTitle:@"Network Test Complete" message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
@@ -532,11 +504,8 @@ static NSMutableSet* hostList;
|
||||
}]];
|
||||
#if !TARGET_OS_TV
|
||||
if (host.state != StateOnline) {
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"NVIDIA GameStream End-of-Service" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ"];
|
||||
}]];
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"];
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
|
||||
}]];
|
||||
}
|
||||
#endif
|
||||
@@ -556,7 +525,9 @@ static NSMutableSet* hostList;
|
||||
longClickAlert.popoverPresentationController.sourceView = view;
|
||||
|
||||
longClickAlert.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view
|
||||
[[self activeViewController] presentViewController:longClickAlert animated:YES completion:nil];
|
||||
[[self activeViewController] presentViewController:longClickAlert animated:YES completion:^{
|
||||
[self updateHosts];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) addHostClicked {
|
||||
@@ -564,7 +535,7 @@ static NSMutableSet* hostList;
|
||||
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:@"If Moonlight doesn't find your local gaming PC automatically,\nenter the IP address of your PC" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
NSString* hostAddress = [((UITextField*)[[alertController textFields] objectAtIndex:0]).text trim];
|
||||
NSString* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
|
||||
[self showLoadingFrame:^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
|
||||
@@ -604,7 +575,6 @@ static NSMutableSet* hostList;
|
||||
- (void) prepareToStreamApp:(TemporaryApp *)app {
|
||||
_streamConfig = [[StreamConfiguration alloc] init];
|
||||
_streamConfig.host = app.host.activeAddress;
|
||||
_streamConfig.httpsPort = app.host.httpsPort;
|
||||
_streamConfig.appID = app.id;
|
||||
_streamConfig.appName = app.name;
|
||||
_streamConfig.serverCert = app.host.serverCert;
|
||||
@@ -635,75 +605,46 @@ static NSMutableSet* hostList;
|
||||
#endif
|
||||
|
||||
_streamConfig.bitRate = [streamSettings.bitrate intValue];
|
||||
_streamConfig.streamingRemotely = streamSettings.streamingRemotely;
|
||||
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
|
||||
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
|
||||
_streamConfig.useFramePacing = streamSettings.useFramePacing;
|
||||
_streamConfig.swapABXYButtons = streamSettings.swapABXYButtons;
|
||||
_streamConfig.allowHevc = streamSettings.useHevc;
|
||||
|
||||
// multiController must be set before calling getConnectedGamepadMask
|
||||
_streamConfig.multiController = streamSettings.multiController;
|
||||
_streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig];
|
||||
|
||||
|
||||
// Probe for supported channel configurations
|
||||
int physicalOutputChannels = (int)[AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
|
||||
Log(LOG_I, @"Audio device supports %d channels", physicalOutputChannels);
|
||||
|
||||
int numberOfChannels = MIN([streamSettings.audioConfig intValue], physicalOutputChannels);
|
||||
Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels);
|
||||
if (numberOfChannels >= 8) {
|
||||
long outputChannels = [AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
|
||||
Log(LOG_I, @"Audio device supports %d channels", outputChannels);
|
||||
if (outputChannels >= 8) {
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
|
||||
}
|
||||
else if (numberOfChannels >= 6) {
|
||||
else if (outputChannels >= 6) {
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
|
||||
_streamConfig.serverCodecModeSupport = app.host.serverCodecModeSupport;
|
||||
|
||||
switch (streamSettings.preferredCodec) {
|
||||
case CODEC_PREF_AV1:
|
||||
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
|
||||
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) {
|
||||
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN8;
|
||||
}
|
||||
#endif
|
||||
// Fall-through
|
||||
|
||||
case CODEC_PREF_AUTO:
|
||||
case CODEC_PREF_HEVC:
|
||||
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
|
||||
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265;
|
||||
}
|
||||
// Fall-through
|
||||
|
||||
case CODEC_PREF_H264:
|
||||
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H264;
|
||||
break;
|
||||
// HDR requires HDR10 game, HDR10 display, and HEVC Main10 decoder on the client.
|
||||
// It additionally requires an HEVC Main10 encoder on the server (GTX 1000+).
|
||||
//
|
||||
// It should also be a user preference, since some games may require higher peak
|
||||
// brightness than the iOS device can support to look correct in HDR mode.
|
||||
if (@available(iOS 11.3, *)) {
|
||||
_streamConfig.enableHdr =
|
||||
app.hdrSupported && // App supported
|
||||
(app.host.serverCodecModeSupport & 0x200) != 0 && // HEVC Main10 encoding on host PC GPU
|
||||
VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) && // Decoder supported
|
||||
(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0 && // Display supported
|
||||
streamSettings.enableHdr; // User wants it enabled
|
||||
}
|
||||
|
||||
// HEVC is supported if the user wants it (or it's required by the chosen resolution) and the SoC supports it
|
||||
if ((_streamConfig.width > 4096 || _streamConfig.height > 4096 || streamSettings.enableHdr) && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
|
||||
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265;
|
||||
|
||||
// HEVC Main10 is supported if the user wants it and the display supports it
|
||||
if (streamSettings.enableHdr && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0) {
|
||||
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265_MAIN10;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
|
||||
// Add the AV1 Main10 format if AV1 and HDR are both enabled and supported
|
||||
if ((_streamConfig.supportedVideoFormats & VIDEO_FORMAT_MASK_AV1) && streamSettings.enableHdr &&
|
||||
VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1) && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0) {
|
||||
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN10;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
|
||||
Log(LOG_D, @"Long clicked app: %@", app.name);
|
||||
- (void) appClicked:(TemporaryApp *)app {
|
||||
Log(LOG_D, @"Clicked app: %@", app.name);
|
||||
|
||||
[_appManager stopRetrieving];
|
||||
|
||||
@@ -715,49 +656,24 @@ static NSMutableSet* hostList;
|
||||
[[self revealViewController] revealToggleAnimated:NO];
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
TemporaryApp* currentApp = [self findRunningApp:app.host];
|
||||
|
||||
NSString* message;
|
||||
|
||||
if (currentApp == nil || [app.id isEqualToString:currentApp.id]) {
|
||||
if (app.hidden) {
|
||||
message = @"Hidden";
|
||||
}
|
||||
else {
|
||||
message = @"";
|
||||
}
|
||||
}
|
||||
else {
|
||||
message = [NSString stringWithFormat:@"%@ is currently running", currentApp.name];
|
||||
}
|
||||
|
||||
UIAlertController* alertController = [UIAlertController
|
||||
alertControllerWithTitle: app.name
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
[alertController addAction:[UIAlertAction
|
||||
actionWithTitle:currentApp == nil ? @"Launch App" : ([app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
if (currentApp != nil) {
|
||||
Log(LOG_I, @"Resuming application: %@", currentApp.name);
|
||||
[self prepareToStreamApp:currentApp];
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"Launching application: %@", app.name);
|
||||
[self prepareToStreamApp:app];
|
||||
}
|
||||
|
||||
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
|
||||
}]];
|
||||
|
||||
if (currentApp != nil) {
|
||||
UIAlertController* alertController = [UIAlertController
|
||||
alertControllerWithTitle: app.name
|
||||
message: [app.id isEqualToString:currentApp.id] ? @"" : [NSString stringWithFormat:@"%@ is currently running", currentApp.name]preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addAction:[UIAlertAction
|
||||
actionWithTitle:[app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
Log(LOG_I, @"Resuming application: %@", currentApp.name);
|
||||
[self prepareToStreamApp:currentApp];
|
||||
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
|
||||
}]];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:
|
||||
[app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
|
||||
Log(LOG_I, @"Quitting application: %@", currentApp.name);
|
||||
[self showLoadingFrame: ^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host];
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host.activeAddress uniqueId:self->_uniqueId serverCert:app.host.serverCert];
|
||||
HttpResponse* quitResponse = [[HttpResponse alloc] init];
|
||||
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
|
||||
|
||||
@@ -798,6 +714,9 @@ static NSMutableSet* hostList;
|
||||
else {
|
||||
app.host.currentGame = @"0";
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Refresh the UI
|
||||
[self updateAppsForHost:app.host];
|
||||
|
||||
// If it succeeds and we're to start streaming, segue to the stream
|
||||
if (![app.id isEqualToString:currentApp.id]) {
|
||||
[self prepareToStreamApp:app];
|
||||
@@ -815,46 +734,8 @@ static NSMutableSet* hostList;
|
||||
}];
|
||||
|
||||
}]];
|
||||
}
|
||||
|
||||
if (currentApp == nil || ![app.id isEqualToString:currentApp.id] || app.hidden) {
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:app.hidden ? @"Show App" : @"Hide App"
|
||||
style:app.hidden ? UIAlertActionStyleDefault : UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction* action) {
|
||||
app.hidden = !app.hidden;
|
||||
[self updateAppEntry:app forHost:app.host];
|
||||
|
||||
// Don't call updateAppsForHost because that will nuke this
|
||||
// app immediately if we're not showing hidden apps.
|
||||
}]];
|
||||
}
|
||||
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
|
||||
// these two lines are required for iPad support of UIAlertSheet
|
||||
alertController.popoverPresentationController.sourceView = view;
|
||||
|
||||
alertController.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view
|
||||
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void) appClicked:(TemporaryApp *)app view:(UIView *)view {
|
||||
Log(LOG_D, @"Clicked app: %@", app.name);
|
||||
|
||||
[_appManager stopRetrieving];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
if (currentPosition != FrontViewPositionLeft) {
|
||||
// This must not be animated because we need the position
|
||||
// to change (and notify our callback to save settings data)
|
||||
// before we call prepareToStreamApp.
|
||||
[[self revealViewController] revealToggleAnimated:NO];
|
||||
}
|
||||
#endif
|
||||
|
||||
if ([self findRunningApp:app.host]) {
|
||||
// If there's a running app, display a menu
|
||||
[self appLongClicked:app view:view];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
|
||||
} else {
|
||||
[self prepareToStreamApp:app];
|
||||
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
|
||||
@@ -883,7 +764,7 @@ static NSMutableSet* hostList;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[self appClicked:_sortedAppList[indexPath.row] view:nil];
|
||||
[self appClicked:_sortedAppList[indexPath.row]];
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -987,11 +868,6 @@ static NSMutableSet* hostList;
|
||||
self.collectionView.allowsMultipleSelection = NO;
|
||||
#if !TARGET_OS_TV
|
||||
self.collectionView.multipleTouchEnabled = NO;
|
||||
#else
|
||||
// This is the only way to get long press events on a UICollectionViewCell :(
|
||||
UILongPressGestureRecognizer* cellLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleCollectionViewLongPress:)];
|
||||
cellLongPress.delaysTouchesBegan = YES;
|
||||
[self.collectionView addGestureRecognizer:cellLongPress];
|
||||
#endif
|
||||
|
||||
[self retrieveSavedHosts];
|
||||
@@ -1007,21 +883,6 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
-(void)handleCollectionViewLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
// FIXME: Something is delaying touches so we only get to the Begin state
|
||||
// before we actually want to signal the long press.
|
||||
if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
|
||||
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
|
||||
if (indexPath != nil) {
|
||||
[self appLongClicked:_sortedAppList[indexPath.row] view:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openTvSettings:(id)sender
|
||||
{
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
|
||||
@@ -1180,17 +1041,14 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
|
||||
- (void) updateAllHosts:(NSArray *)hosts {
|
||||
// We must copy the array here because it could be modified
|
||||
// before our main thread dispatch happens.
|
||||
NSArray* hostsCopy = [NSArray arrayWithArray:hosts];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
Log(LOG_D, @"New host list:");
|
||||
for (TemporaryHost* host in hostsCopy) {
|
||||
for (TemporaryHost* host in hosts) {
|
||||
Log(LOG_D, @"Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \n\t ipv6Address:%@ \n\t uuid:%@ \n\t mac:%@ \n\t pairState:%d \n\t online:%d \n\t activeAddress:%@ \n}", host.name, host.address, host.localAddress, host.externalAddress, host.ipv6Address, host.uuid, host.mac, host.pairState, host.state, host.activeAddress);
|
||||
}
|
||||
@synchronized(hostList) {
|
||||
[hostList removeAllObjects];
|
||||
[hostList addObjectsFromArray:hostsCopy];
|
||||
[hostList addObjectsFromArray:hosts];
|
||||
}
|
||||
[self updateHosts];
|
||||
});
|
||||
@@ -1198,25 +1056,27 @@ static NSMutableSet* hostList;
|
||||
|
||||
- (void)updateHostShortcuts {
|
||||
#if !TARGET_OS_TV
|
||||
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
|
||||
|
||||
@synchronized (hostList) {
|
||||
for (TemporaryHost* host in hostList) {
|
||||
// Pair state may be unknown if we haven't polled it yet, but the app list
|
||||
// count will persist from paired PCs
|
||||
if ([host.appList count] > 0) {
|
||||
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
|
||||
initWithType:@"PC"
|
||||
localizedTitle:host.name
|
||||
localizedSubtitle:nil
|
||||
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
|
||||
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
|
||||
[quickActions addObject: shortcut];
|
||||
if (@available(iOS 9.0, *)) {
|
||||
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
|
||||
|
||||
@synchronized (hostList) {
|
||||
for (TemporaryHost* host in hostList) {
|
||||
// Pair state may be unknown if we haven't polled it yet, but the app list
|
||||
// count will persist from paired PCs
|
||||
if ([host.appList count] > 0) {
|
||||
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
|
||||
initWithType:@"PC"
|
||||
localizedTitle:host.name
|
||||
localizedSubtitle:nil
|
||||
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
|
||||
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
|
||||
[quickActions addObject: shortcut];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UIApplication sharedApplication].shortcutItems = quickActions;
|
||||
}
|
||||
|
||||
[UIApplication sharedApplication].shortcutItems = quickActions;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1329,16 +1189,6 @@ static NSMutableSet* hostList;
|
||||
_sortedAppList = [host.appList allObjects];
|
||||
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
|
||||
|
||||
if (!_showHiddenApps) {
|
||||
NSMutableArray* visibleAppList = [NSMutableArray array];
|
||||
for (TemporaryApp* app in _sortedAppList) {
|
||||
if (!app.hidden) {
|
||||
[visibleAppList addObject:app];
|
||||
}
|
||||
}
|
||||
_sortedAppList = visibleAppList;
|
||||
}
|
||||
|
||||
[hostScrollView removeFromSuperview];
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
@@ -1357,13 +1207,13 @@ static NSMutableSet* hostList;
|
||||
|
||||
[cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added
|
||||
[cell addSubview:appView];
|
||||
|
||||
|
||||
// Shadow opacity is controlled inside UIAppView based on whether the app
|
||||
// is hidden or not during the update cycle.
|
||||
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.bounds];
|
||||
cell.layer.masksToBounds = NO;
|
||||
cell.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f);
|
||||
cell.layer.shadowOpacity = 0.5f;
|
||||
cell.layer.shadowPath = shadowPath.CGPath;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
@@ -626,10 +626,10 @@ const int FrontViewPositionNone = 0xff;
|
||||
_frontViewPosition = FrontViewPositionLeft;
|
||||
_rearViewPosition = FrontViewPositionLeft;
|
||||
_rightViewPosition = FrontViewPositionLeft;
|
||||
_rearViewRevealWidth = 510.0f;
|
||||
_rearViewRevealWidth = 260.0f;
|
||||
_rearViewRevealOverdraw = 60.0f;
|
||||
_rearViewRevealDisplacement = 40.0f;
|
||||
_rightViewRevealWidth = 510.0f;
|
||||
_rightViewRevealWidth = 260.0f;
|
||||
_rightViewRevealOverdraw = 60.0f;
|
||||
_rightViewRevealDisplacement = 40.0f;
|
||||
_bounceBackOnOverdraw = YES;
|
||||
@@ -1387,11 +1387,15 @@ const int FrontViewPositionNone = 0xff;
|
||||
|
||||
// These are derived from the primary view controller
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
|
||||
}
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[self setNeedsUpdateOfPrefersPointerLocked];
|
||||
// iOS 11 betas (which are still installed in some places :\) crash with
|
||||
// doesNotRecognizeSelector since this wasn't added until after the iPhone X
|
||||
// announcement.
|
||||
if ([self respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
if ([self respondsToSelector: @selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]) {
|
||||
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1405,11 +1409,6 @@ const int FrontViewPositionNone = 0xff;
|
||||
return _primaryViewController;
|
||||
}
|
||||
|
||||
- (UIViewController*)childViewControllerForPointerLock
|
||||
{
|
||||
return _primaryViewController;
|
||||
}
|
||||
|
||||
#pragma mark Animated view controller deployment and layout
|
||||
|
||||
// Primitive method for view controller deployment and animated layout to the given position.
|
||||
|
||||