Compare commits

..
5 Commits
Author SHA1 Message Date
Cameron Gutman 882b61ac63 Implement GCMouse API for relative mouse support
This seems to be quite broken in iOS 14.0. The Microsoft Surface Precision mouse is detected as a GCKeyboard instead of a GCMouse.
2020-09-17 13:56:09 -05:00
Cameron Gutman 4c68a5d8c3 Disable iOS 13.4+ mouse support on iOS 14 in preparation for GCMouse work 2020-09-16 18:36:49 -07:00
Cameron Gutman 452b001c99 Raise iOS deployment target to iOS 9.3 as required by Xcode 12 2020-09-16 17:17:25 -07:00
Cameron Gutman cac639806c Replace Citrix X1 toggle with touchscreen mode toggle 2020-09-16 17:13:26 -07:00
Cameron Gutman dbca1bb4ce Remove Citrix X1 support in preparation for GCMouse integration 2020-09-16 17:08:20 -07:00
480 changed files with 35881 additions and 92080 deletions
-48
View File
@@ -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
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
-17
View File
@@ -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.
-25
View File
@@ -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
-45
View File
@@ -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
-2
View File
@@ -1,6 +1,4 @@
**/xcuserdata/ **/xcuserdata/
**/Moonlight.xcscmblueprint **/Moonlight.xcscmblueprint
**/xcschemes/*.xcscheme
.DS_Store
Build Build
DerivedData DerivedData
-3
View File
@@ -1,6 +1,3 @@
[submodule "moonlight-common/moonlight-common-c"] [submodule "moonlight-common/moonlight-common-c"]
path = moonlight-common/moonlight-common-c path = moonlight-common/moonlight-common-c
url = https://github.com/moonlight-stream/moonlight-common-c.git url = https://github.com/moonlight-stream/moonlight-common-c.git
[submodule "X1Kit"]
path = X1Kit
url = https://github.com/cgutman/X1Kit.git
+25
View File
@@ -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
+144
View File
@@ -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
+51
View File
@@ -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
+36
View File
@@ -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
+20
View File
@@ -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
+104
View File
@@ -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"
-180
View File
@@ -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."
+124
View File
@@ -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"
-8
View File
@@ -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 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 Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>. <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.
+5 -3
View File
@@ -24,9 +24,11 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#if !TARGET_OS_TV #if !TARGET_OS_TV
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey]; if (@available(iOS 9.0, *)) {
if (shortcut != nil) { UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"]; if (shortcut != nil) {
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
}
} }
#endif #endif
return YES; return YES;
-2
View File
@@ -96,7 +96,6 @@ static NSData* p12 = nil;
bio = BIO_new(BIO_s_mem()); bio = BIO_new(BIO_s_mem());
i2d_X509_bio(bio, x509); i2d_X509_bio(bio, x509);
X509_free(x509);
BUF_MEM* mem; BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem); BIO_get_mem_ptr(bio, &mem);
@@ -223,7 +222,6 @@ static NSData* p12 = nil;
+ (NSData *)getSignatureFromCert:(NSData *)cert { + (NSData *)getSignatureFromCert:(NSData *)cert {
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]); BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL); X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free(bio);
if (!x509) { if (!x509) {
Log(LOG_E, @"Unable to parse certificate in memory!"); Log(LOG_E, @"Unable to parse certificate in memory!");
+8 -18
View File
@@ -4,12 +4,10 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/pem.h> #include <openssl/pem.h>
#include <OpenSSL/provider.h> #include <openssl/rsa.h>
#include <OpenSSL/rsa.h>
#include <openssl/x509.h> #include <openssl/x509.h>
#include <OpenSSL/rand.h> #include <openssl/rand.h>
static const int NUM_BITS = 2048; static const int NUM_BITS = 2048;
static const int SERIAL = 0; 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) { struct CertKeyPair generateCertKeyPair(void) {
BIO *bio_err;
X509 *x509 = NULL; X509 *x509 = NULL;
EVP_PKEY *pkey = NULL; EVP_PKEY *pkey = NULL;
PKCS12 *p12 = NULL; PKCS12 *p12 = NULL;
bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS); mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
char* pass = "limelight"; p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
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());
if (p12 == NULL) { if (p12 == NULL) {
printf("Error generating a valid PKCS12 certificate.\n"); printf("Error generating a valid PKCS12 certificate.\n");
} }
BIO_free(bio_err);
return (CertKeyPair){x509, pkey, p12}; return (CertKeyPair){x509, pkey, p12};
} }
+2 -7
View File
@@ -17,18 +17,13 @@
framerate:(NSInteger)framerate framerate:(NSInteger)framerate
height:(NSInteger)height height:(NSInteger)height
width:(NSInteger)width width:(NSInteger)width
audioConfig:(NSInteger)audioConfig
onscreenControls:(NSInteger)onscreenControls onscreenControls:(NSInteger)onscreenControls
optimizeGames:(BOOL)optimizeGames optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController multiController:(BOOL)multiController
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC audioOnPC:(BOOL)audioOnPC
preferredCodec:(uint32_t)preferredCodec useHevc:(BOOL)useHevc
useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport absoluteTouchMode:(BOOL)absoluteTouchMode;
absoluteTouchMode:(BOOL)absoluteTouchMode
statsOverlay:(BOOL)statsOverlay;
- (NSArray*) getHosts; - (NSArray*) getHosts;
- (void) updateHost:(TemporaryHost*)host; - (void) updateHost:(TemporaryHost*)host;
+6 -15
View File
@@ -21,11 +21,12 @@
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep // HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
// Main Thread Checker happy. // Main Thread Checker happy.
if ([NSThread isMainThread]) { if ([NSThread isMainThread]) {
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; _appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
} }
else { else {
dispatch_sync(dispatch_get_main_queue(), ^{ 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 framerate:(NSInteger)framerate
height:(NSInteger)height height:(NSInteger)height
width:(NSInteger)width width:(NSInteger)width
audioConfig:(NSInteger)audioConfig
onscreenControls:(NSInteger)onscreenControls onscreenControls:(NSInteger)onscreenControls
optimizeGames:(BOOL)optimizeGames optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController multiController:(BOOL)multiController
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC audioOnPC:(BOOL)audioOnPC
preferredCodec:(uint32_t)preferredCodec useHevc:(BOOL)useHevc
useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport absoluteTouchMode:(BOOL)absoluteTouchMode {
absoluteTouchMode:(BOOL)absoluteTouchMode
statsOverlay:(BOOL)statsOverlay {
[_managedObjectContext performBlockAndWait:^{ [_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings]; Settings* settingsToSave = [self retrieveSettings];
@@ -75,18 +71,13 @@
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate]; settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
settingsToSave.height = [NSNumber numberWithInteger:height]; settingsToSave.height = [NSNumber numberWithInteger:height];
settingsToSave.width = [NSNumber numberWithInteger:width]; settingsToSave.width = [NSNumber numberWithInteger:width];
settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig];
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls]; settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
settingsToSave.optimizeGames = optimizeGames; settingsToSave.optimizeGames = optimizeGames;
settingsToSave.multiController = multiController; settingsToSave.multiController = multiController;
settingsToSave.swapABXYButtons = swapABXYButtons;
settingsToSave.playAudioOnPC = audioOnPC; settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.preferredCodec = preferredCodec; settingsToSave.useHevc = useHevc;
settingsToSave.useFramePacing = useFramePacing;
settingsToSave.enableHdr = enableHdr; settingsToSave.enableHdr = enableHdr;
settingsToSave.btMouseSupport = btMouseSupport;
settingsToSave.absoluteTouchMode = absoluteTouchMode; settingsToSave.absoluteTouchMode = absoluteTouchMode;
settingsToSave.statsOverlay = statsOverlay;
[self saveData]; [self saveData];
}]; }];
-1
View File
@@ -15,7 +15,6 @@
@property (nullable, nonatomic, retain) NSString *name; @property (nullable, nonatomic, retain) NSString *name;
@property (nullable, nonatomic, retain) NSString *installPath; @property (nullable, nonatomic, retain) NSString *installPath;
@property (nonatomic) BOOL hdrSupported; @property (nonatomic) BOOL hdrSupported;
@property (nonatomic) BOOL hidden;
@property (nullable, nonatomic, retain) TemporaryHost *host; @property (nullable, nonatomic, retain) TemporaryHost *host;
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
-2
View File
@@ -16,7 +16,6 @@
self.id = app.id; self.id = app.id;
self.name = app.name; self.name = app.name;
self.hdrSupported = app.hdrSupported; self.hdrSupported = app.hdrSupported;
self.hidden = app.hidden;
self.host = tempHost; self.host = tempHost;
return self; return self;
@@ -26,7 +25,6 @@
parent.id = self.id; parent.id = self.id;
parent.name = self.name; parent.name = self.name;
parent.hdrSupported = self.hdrSupported; parent.hdrSupported = self.hdrSupported;
parent.hidden = self.hidden;
parent.host = host; parent.host = host;
} }
-2
View File
@@ -15,8 +15,6 @@
@property (atomic) PairState pairState; @property (atomic) PairState pairState;
@property (atomic, nullable, retain) NSString * activeAddress; @property (atomic, nullable, retain) NSString * activeAddress;
@property (atomic, nullable, retain) NSString * currentGame; @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) NSData *serverCert;
@property (atomic, nullable, retain) NSString *address; @property (atomic, nullable, retain) NSString *address;
-5
View File
@@ -34,11 +34,6 @@
self.serverCodecModeSupport = host.serverCodecModeSupport; self.serverCodecModeSupport = host.serverCodecModeSupport;
self.serverCert = host.serverCert; 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 // 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; self.pairState = host.serverCert ? [host.pairState intValue] : PairStateUnpaired;
+2 -11
View File
@@ -16,24 +16,15 @@
@property (nonatomic, retain) NSNumber * framerate; @property (nonatomic, retain) NSNumber * framerate;
@property (nonatomic, retain) NSNumber * height; @property (nonatomic, retain) NSNumber * height;
@property (nonatomic, retain) NSNumber * width; @property (nonatomic, retain) NSNumber * width;
@property (nonatomic, retain) NSNumber * audioConfig;
@property (nonatomic, retain) NSNumber * onscreenControls; @property (nonatomic, retain) NSNumber * onscreenControls;
@property (nonatomic, retain) NSString * uniqueId; @property (nonatomic, retain) NSString * uniqueId;
@property (nonatomic) enum { @property (nonatomic) BOOL streamingRemotely;
CODEC_PREF_AUTO, @property (nonatomic) BOOL useHevc;
CODEC_PREF_H264,
CODEC_PREF_HEVC,
CODEC_PREF_AV1,
} preferredCodec;
@property (nonatomic) BOOL useFramePacing;
@property (nonatomic) BOOL multiController; @property (nonatomic) BOOL multiController;
@property (nonatomic) BOOL swapABXYButtons;
@property (nonatomic) BOOL playAudioOnPC; @property (nonatomic) BOOL playAudioOnPC;
@property (nonatomic) BOOL optimizeGames; @property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr; @property (nonatomic) BOOL enableHdr;
@property (nonatomic) BOOL btMouseSupport;
@property (nonatomic) BOOL absoluteTouchMode; @property (nonatomic) BOOL absoluteTouchMode;
@property (nonatomic) BOOL statsOverlay;
- (id) initFromSettings:(Settings*)settings; - (id) initFromSettings:(Settings*)settings;
+2 -17
View File
@@ -34,17 +34,11 @@
assert([self.bitrate intValue] != 0); assert([self.bitrate intValue] != 0);
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]]; self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
assert([self.framerate intValue] != 0); assert([self.framerate intValue] != 0);
self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]]; self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"];
assert([self.audioConfig intValue] != 0);
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"]; self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"]; self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"]; self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"]; 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"]; NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) { switch (_screenSize) {
@@ -60,10 +54,6 @@
self.height = [NSNumber numberWithInteger:2160]; self.height = [NSNumber numberWithInteger:2160];
self.width = [NSNumber numberWithInteger:3840]; self.width = [NSNumber numberWithInteger:3840];
break; break;
case 3:
self.height = [NSNumber numberWithInteger:1440];
self.width = [NSNumber numberWithInteger:2560];
break;
default: default:
abort(); abort();
} }
@@ -73,18 +63,13 @@
self.framerate = settings.framerate; self.framerate = settings.framerate;
self.height = settings.height; self.height = settings.height;
self.width = settings.width; self.width = settings.width;
self.audioConfig = settings.audioConfig; self.useHevc = settings.useHevc;
self.preferredCodec = settings.preferredCodec;
self.useFramePacing = settings.useFramePacing;
self.playAudioOnPC = settings.playAudioOnPC; self.playAudioOnPC = settings.playAudioOnPC;
self.enableHdr = settings.enableHdr; self.enableHdr = settings.enableHdr;
self.optimizeGames = settings.optimizeGames; self.optimizeGames = settings.optimizeGames;
self.multiController = settings.multiController; self.multiController = settings.multiController;
self.swapABXYButtons = settings.swapABXYButtons;
self.onscreenControls = settings.onscreenControls; self.onscreenControls = settings.onscreenControls;
self.btMouseSupport = settings.btMouseSupport;
self.absoluteTouchMode = settings.absoluteTouchMode; self.absoluteTouchMode = settings.absoluteTouchMode;
self.statsOverlay = settings.statsOverlay;
#endif #endif
self.uniqueId = settings.uniqueId; self.uniqueId = settings.uniqueId;
Binary file not shown.

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
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"info" : { "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
}
}
Binary file not shown.

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
}
}
Binary file not shown.

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
}
}
Binary file not shown.

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
}
}
Binary file not shown.

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
}
}
Binary file not shown.

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
}
}
Binary file not shown.

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
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

-19
View File
@@ -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
-120
View File
@@ -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
+2 -28
View File
@@ -6,18 +6,10 @@
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved. // Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
// //
#import "HapticContext.h"
@import GameController; @import GameController;
@import CoreHaptics;
@interface Controller : NSObject @interface Controller : NSObject
typedef struct {
float lastX;
float lastY;
} controller_touch_context_t;
@property (nullable, nonatomic, retain) GCController* gamepad; @property (nullable, nonatomic, retain) GCController* gamepad;
@property (nonatomic) int playerIndex; @property (nonatomic) int playerIndex;
@property (nonatomic) int lastButtonFlags; @property (nonatomic) int lastButtonFlags;
@@ -29,25 +21,7 @@ typedef struct {
@property (nonatomic) short lastLeftStickY; @property (nonatomic) short lastLeftStickY;
@property (nonatomic) short lastRightStickX; @property (nonatomic) short lastRightStickX;
@property (nonatomic) short lastRightStickY; @property (nonatomic) short lastRightStickY;
@property (nonatomic) unsigned short lowFreqMotor;
@property (nonatomic) controller_touch_context_t primaryTouch; @property (nonatomic) unsigned short highFreqMotor;
@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;
@end @end
+2 -8
View File
@@ -11,18 +11,15 @@
@class OnScreenControls; @class OnScreenControls;
@protocol ControllerSupportDelegate <NSObject> @protocol GamepadPresenceDelegate <NSObject>
- (void) gamepadPresenceChanged; - (void) gamepadPresenceChanged;
- (void) mousePresenceChanged;
- (void) streamExitRequested;
@end @end
@interface ControllerSupport : NSObject @interface ControllerSupport : NSObject
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate; -(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<GamepadPresenceDelegate>)delegate;
-(void) connectionEstablished;
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc; -(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
-(void) cleanup; -(void) cleanup;
@@ -42,9 +39,6 @@
-(void) updateFinished:(Controller*)controller; -(void) updateFinished:(Controller*)controller;
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor; -(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; +(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
File diff suppressed because it is too large Load Diff
-22
View File
@@ -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
-170
View File
@@ -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
-18
View File
@@ -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
-18
View File
@@ -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
-1
View File
@@ -16,7 +16,6 @@ struct KeyEvent {
u_char modifier; 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)); + (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags; + (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
+5 -56
View File
@@ -11,40 +11,7 @@
@implementation KeyboardSupport @implementation KeyboardSupport
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4)) { + (BOOL)sendKeyEvent:(UIKey*)key 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)) {
char modifierFlags = 0; char modifierFlags = 0;
short keyCode = 0; short keyCode = 0;
@@ -54,12 +21,9 @@
if (key.modifierFlags & UIKeyModifierAlternate) { if (key.modifierFlags & UIKeyModifierAlternate) {
modifierFlags |= MODIFIER_ALT; modifierFlags |= MODIFIER_ALT;
} }
if (key.modifierFlags & UIKeyModifierControl) { if (key.modifierFlags & (UIKeyModifierControl | UIKeyModifierCommand)) {
modifierFlags |= MODIFIER_CTRL; modifierFlags |= MODIFIER_CTRL;
} }
if (key.modifierFlags & UIKeyModifierCommand) {
modifierFlags |= MODIFIER_META;
}
// This converts UIKeyboardHIDUsage values to Win32 VK_* values // This converts UIKeyboardHIDUsage values to Win32 VK_* values
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes // https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
@@ -201,8 +165,7 @@
keyCode = 0x6B; keyCode = 0x6B;
break; break;
case UIKeyboardHIDUsageKeypadEnter: case UIKeyboardHIDUsageKeypadEnter:
keyCode = 0x0D; keyCode = 0x0D; // FIXME: Is this correct?
modifierFlags |= MODIFIER_EXTENDED;
break; break;
case UIKeyboardHIDUsageKeypadPeriod: case UIKeyboardHIDUsageKeypadPeriod:
keyCode = 0x6E; keyCode = 0x6E;
@@ -226,8 +189,6 @@
keyCode = 0xF8; keyCode = 0xF8;
break; break;
case UIKeyboardHIDUsageKeyboardLeftGUI: case UIKeyboardHIDUsageKeyboardLeftGUI:
keyCode = 0x5B;
break;
case UIKeyboardHIDUsageKeyboardLeftControl: case UIKeyboardHIDUsageKeyboardLeftControl:
keyCode = 0xA2; keyCode = 0xA2;
break; break;
@@ -238,8 +199,6 @@
keyCode = 0xA4; keyCode = 0xA4;
break; break;
case UIKeyboardHIDUsageKeyboardRightGUI: case UIKeyboardHIDUsageKeyboardRightGUI:
keyCode = 0x5C;
break;
case UIKeyboardHIDUsageKeyboardRightControl: case UIKeyboardHIDUsageKeyboardRightControl:
keyCode = 0xA3; keyCode = 0xA3;
break; break;
@@ -249,9 +208,6 @@
case UIKeyboardHIDUsageKeyboardRightAlt: case UIKeyboardHIDUsageKeyboardRightAlt:
keyCode = 0xA5; keyCode = 0xA5;
break; 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: default:
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode); NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
assert(0); assert(0);
@@ -259,7 +215,7 @@
} }
} }
LiSendKeyboardEvent(0x8000 | keyCode, LiSendKeyboardEvent(keyCode,
down ? KEY_ACTION_DOWN : KEY_ACTION_UP, down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
modifierFlags); modifierFlags);
return true; return true;
@@ -276,12 +232,10 @@
case UIKeyModifierShift: case UIKeyModifierShift:
[KeyboardSupport addShiftModifier:&event]; [KeyboardSupport addShiftModifier:&event];
break; break;
case UIKeyModifierCommand:
case UIKeyModifierControl: case UIKeyModifierControl:
[KeyboardSupport addControlModifier:&event]; [KeyboardSupport addControlModifier:&event];
break; break;
case UIKeyModifierCommand:
[KeyboardSupport addMetaModifier:&event];
break;
case UIKeyModifierAlternate: case UIKeyModifierAlternate:
[KeyboardSupport addAltModifier:&event]; [KeyboardSupport addAltModifier:&event];
break; break;
@@ -439,11 +393,6 @@
event->modifierKeycode = 0x11; event->modifierKeycode = 0x11;
} }
+ (void) addMetaModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_META;
event->modifierKeycode = 0x5B;
}
+ (void) addAltModifier:(struct KeyEvent*)event { + (void) addAltModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_ALT; event->modifier = MODIFIER_ALT;
event->modifierKeycode = 0x12; event->modifierKeycode = 0x12;
+2 -2
View File
@@ -8,8 +8,8 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@protocol EdgeDetectionDelegate;
@class ControllerSupport; @class ControllerSupport;
@class StreamConfiguration;
@interface OnScreenControls : NSObject @interface OnScreenControls : NSObject
@@ -25,7 +25,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons 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) handleTouchDownEvent:(NSSet*)touches;
- (BOOL) handleTouchUpEvent:(NSSet*)touches; - (BOOL) handleTouchUpEvent:(NSSet*)touches;
- (BOOL) handleTouchMovedEvent:(NSSet*)touches; - (BOOL) handleTouchMovedEvent:(NSSet*)touches;
+47 -36
View File
@@ -36,6 +36,7 @@
CALayer* _l1Button; CALayer* _l1Button;
CALayer* _l2Button; CALayer* _l2Button;
CALayer* _l3Button; CALayer* _l3Button;
CALayer* _edge;
UITouch* _aTouch; UITouch* _aTouch;
UITouch* _bTouch; UITouch* _bTouch;
@@ -52,6 +53,7 @@
UITouch* _l1Touch; UITouch* _l1Touch;
UITouch* _l2Touch; UITouch* _l2Touch;
UITouch* _l3Touch; UITouch* _l3Touch;
UITouch* _edgeTouch;
NSDate* l3TouchStart; NSDate* l3TouchStart;
NSDate* r3TouchStart; NSDate* r3TouchStart;
@@ -67,8 +69,8 @@
ControllerSupport *_controllerSupport; ControllerSupport *_controllerSupport;
Controller *_controller; Controller *_controller;
id<EdgeDetectionDelegate> _edgeDelegate;
NSMutableArray* _deadTouches; NSMutableArray* _deadTouches;
BOOL _swapABXY;
} }
static const float EDGE_WIDTH = .05; static const float EDGE_WIDTH = .05;
@@ -112,13 +114,13 @@ static float L2_Y;
static float L3_X; static float L3_X;
static float L3_Y; 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]; self = [self init];
_view = view; _view = view;
_controllerSupport = controllerSupport; _controllerSupport = controllerSupport;
_controller = [controllerSupport getOscController]; _controller = [controllerSupport getOscController];
_edgeDelegate = swipeDelegate;
_deadTouches = [[NSMutableArray alloc] init]; _deadTouches = [[NSMutableArray alloc] init];
_swapABXY = streamConfig.swapABXYButtons;
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad); _iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height); _controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
@@ -154,6 +156,7 @@ static float L3_Y;
_rightStickBackground = [CALayer layer]; _rightStickBackground = [CALayer layer];
_leftStick = [CALayer layer]; _leftStick = [CALayer layer];
_rightStick = [CALayer layer]; _rightStick = [CALayer layer];
_edge = [CALayer layer];
return self; return self;
} }
@@ -161,6 +164,8 @@ static float L3_Y;
- (void) show { - (void) show {
_visible = YES; _visible = YES;
[self setupEdgeDetection];
[self updateControls]; [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 // For GCExtendedGamepad controls we move start, select, L3, and R3 to the button
- (void) setupExtendedGamepadControls { - (void) setupExtendedGamepadControls {
// Start with the default complex layout // Start with the default complex layout
@@ -374,33 +384,27 @@ static float L3_Y;
} }
- (void) drawButtons { - (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 // create A button
UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
_aButton.contents = (id) aButtonImage.CGImage; _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]; [_view.layer addSublayer:_aButton];
// create B button // 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; _bButton.contents = (id) bButtonImage.CGImage;
[_view.layer addSublayer:_bButton]; [_view.layer addSublayer:_bButton];
// create X Button // 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; _xButton.contents = (id) xButtonImage.CGImage;
[_view.layer addSublayer:_xButton]; [_view.layer addSublayer:_xButton];
// create Y Button // 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; _yButton.contents = (id) yButtonImage.CGImage;
[_view.layer addSublayer:_yButton]; [_view.layer addSublayer:_yButton];
@@ -654,7 +658,7 @@ static float L3_Y;
if (updated) { if (updated) {
[_controllerSupport updateFinished:_controller]; [_controllerSupport updateFinished:_controller];
} }
return updated || buttonTouch; return updated || buttonTouch;
} }
- (BOOL)handleTouchDownEvent:touches { - (BOOL)handleTouchDownEvent:touches {
@@ -663,63 +667,63 @@ static float L3_Y;
for (UITouch* touch in touches) { for (UITouch* touch in touches) {
CGPoint touchLocation = [touch locationInView:_view]; CGPoint touchLocation = [touch locationInView:_view];
if (_aButton.superlayer != nil && [_aButton.presentationLayer hitTest:touchLocation]) { if ([_aButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:A_FLAG]; [_controllerSupport setButtonFlag:_controller flags:A_FLAG];
_aTouch = touch; _aTouch = touch;
updated = true; updated = true;
} else if (_bButton.superlayer != nil && [_bButton.presentationLayer hitTest:touchLocation]) { } else if ([_bButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:B_FLAG]; [_controllerSupport setButtonFlag:_controller flags:B_FLAG];
_bTouch = touch; _bTouch = touch;
updated = true; updated = true;
} else if (_xButton.superlayer != nil && [_xButton.presentationLayer hitTest:touchLocation]) { } else if ([_xButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:X_FLAG]; [_controllerSupport setButtonFlag:_controller flags:X_FLAG];
_xTouch = touch; _xTouch = touch;
updated = true; updated = true;
} else if (_yButton.superlayer != nil && [_yButton.presentationLayer hitTest:touchLocation]) { } else if ([_yButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:Y_FLAG]; [_controllerSupport setButtonFlag:_controller flags:Y_FLAG];
_yTouch = touch; _yTouch = touch;
updated = true; updated = true;
} else if (_upButton.superlayer != nil && [_upButton.presentationLayer hitTest:touchLocation]) { } else if ([_upButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG]; [_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
_dpadTouch = touch; _dpadTouch = touch;
updated = true; updated = true;
} else if (_downButton.superlayer != nil && [_downButton.presentationLayer hitTest:touchLocation]) { } else if ([_downButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG]; [_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
_dpadTouch = touch; _dpadTouch = touch;
updated = true; updated = true;
} else if (_leftButton.superlayer != nil && [_leftButton.presentationLayer hitTest:touchLocation]) { } else if ([_leftButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG]; [_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
_dpadTouch = touch; _dpadTouch = touch;
updated = true; updated = true;
} else if (_rightButton.superlayer != nil && [_rightButton.presentationLayer hitTest:touchLocation]) { } else if ([_rightButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG]; [_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
_dpadTouch = touch; _dpadTouch = touch;
updated = true; updated = true;
} else if (_startButton.superlayer != nil && [_startButton.presentationLayer hitTest:touchLocation]) { } else if ([_startButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG]; [_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG];
_startTouch = touch; _startTouch = touch;
updated = true; updated = true;
} else if (_selectButton.superlayer != nil && [_selectButton.presentationLayer hitTest:touchLocation]) { } else if ([_selectButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:BACK_FLAG]; [_controllerSupport setButtonFlag:_controller flags:BACK_FLAG];
_selectTouch = touch; _selectTouch = touch;
updated = true; updated = true;
} else if (_l1Button.superlayer != nil && [_l1Button.presentationLayer hitTest:touchLocation]) { } else if ([_l1Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:LB_FLAG]; [_controllerSupport setButtonFlag:_controller flags:LB_FLAG];
_l1Touch = touch; _l1Touch = touch;
updated = true; updated = true;
} else if (_r1Button.superlayer != nil && [_r1Button.presentationLayer hitTest:touchLocation]) { } else if ([_r1Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:RB_FLAG]; [_controllerSupport setButtonFlag:_controller flags:RB_FLAG];
_r1Touch = touch; _r1Touch = touch;
updated = true; updated = true;
} else if (_l2Button.superlayer != nil && [_l2Button.presentationLayer hitTest:touchLocation]) { } else if ([_l2Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport updateLeftTrigger:_controller left:0xFF]; [_controllerSupport updateLeftTrigger:_controller left:0xFF];
_l2Touch = touch; _l2Touch = touch;
updated = true; updated = true;
} else if (_r2Button.superlayer != nil && [_r2Button.presentationLayer hitTest:touchLocation]) { } else if ([_r2Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport updateRightTrigger:_controller right:0xFF]; [_controllerSupport updateRightTrigger:_controller right:0xFF];
_r2Touch = touch; _r2Touch = touch;
updated = true; updated = true;
} else if (_l3Button.superlayer != nil && [_l3Button.presentationLayer hitTest:touchLocation]) { } else if ([_l3Button.presentationLayer hitTest:touchLocation]) {
if (l3Set) { if (l3Set) {
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG]; [_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
_l3Button.borderWidth = 0.0f; _l3Button.borderWidth = 0.0f;
@@ -730,7 +734,7 @@ static float L3_Y;
l3Set = !l3Set; l3Set = !l3Set;
_l3Touch = touch; _l3Touch = touch;
updated = true; updated = true;
} else if (_r3Button.superlayer != nil && [_r3Button.presentationLayer hitTest:touchLocation]) { } else if ([_r3Button.presentationLayer hitTest:touchLocation]) {
if (r3Set) { if (r3Set) {
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG]; [_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
_r3Button.borderWidth = 0.0f; _r3Button.borderWidth = 0.0f;
@@ -741,7 +745,7 @@ static float L3_Y;
r3Set = !r3Set; r3Set = !r3Set;
_r3Touch = touch; _r3Touch = touch;
updated = true; updated = true;
} else if (_leftStick.superlayer != nil && [_leftStick.presentationLayer hitTest:touchLocation]) { } else if ([_leftStick.presentationLayer hitTest:touchLocation]) {
if (l3TouchStart != nil) { if (l3TouchStart != nil) {
// Find elapsed time and convert to milliseconds // Find elapsed time and convert to milliseconds
// Use (-) modifier to conversion since receiver is earlier than now // Use (-) modifier to conversion since receiver is earlier than now
@@ -753,7 +757,7 @@ static float L3_Y;
} }
_lsTouch = touch; _lsTouch = touch;
stickTouch = true; stickTouch = true;
} else if (_rightStick.superlayer != nil && [_rightStick.presentationLayer hitTest:touchLocation]) { } else if ([_rightStick.presentationLayer hitTest:touchLocation]) {
if (r3TouchStart != nil) { if (r3TouchStart != nil) {
// Find elapsed time and convert to milliseconds // Find elapsed time and convert to milliseconds
// Use (-) modifier to conversion since receiver is earlier than now // Use (-) modifier to conversion since receiver is earlier than now
@@ -765,6 +769,8 @@ static float L3_Y;
} }
_rsTouch = touch; _rsTouch = touch;
stickTouch = true; stickTouch = true;
} else if ([_edge.presentationLayer hitTest:touchLocation]) {
_edgeTouch = touch;
} }
if (!updated && !stickTouch && [self isInDeadZone:touch]) { if (!updated && !stickTouch && [self isInDeadZone:touch]) {
[_deadTouches addObject:touch]; [_deadTouches addObject:touch];
@@ -848,6 +854,11 @@ static float L3_Y;
else if (touch == _r3Touch) { else if (touch == _r3Touch) {
_r3Touch = nil; _r3Touch = nil;
touched = true; touched = true;
} else if (touch == _edgeTouch) {
_edgeTouch = nil;
if ([touch locationInView:_view].x >= _view.frame.size.width / 4) {
[_edgeDelegate edgeSwiped];
}
} }
if ([_deadTouches containsObject:touch]) { if ([_deadTouches containsObject:touch]) {
[_deadTouches removeObject:touch]; [_deadTouches removeObject:touch];
-19
View File
@@ -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
-207
View File
@@ -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
+11 -7
View File
@@ -8,9 +8,14 @@
#import "ControllerSupport.h" #import "ControllerSupport.h"
#import "OnScreenControls.h" #import "OnScreenControls.h"
#import "Moonlight-Swift.h"
#import "StreamConfiguration.h" #import "StreamConfiguration.h"
@protocol EdgeDetectionDelegate <NSObject>
- (void) edgeSwiped;
@end
@protocol UserInteractionDelegate <NSObject> @protocol UserInteractionDelegate <NSObject>
- (void) userInteractionBegan; - (void) userInteractionBegan;
@@ -19,19 +24,18 @@
@end @end
#if TARGET_OS_TV #if TARGET_OS_TV
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate> @interface StreamView : OSView <UITextFieldDelegate>
#else #else
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate> @interface StreamView : OSView <UITextFieldDelegate, UIPointerInteractionDelegate>
#endif #endif
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
- (void) setupStreamView:(ControllerSupport*)controllerSupport - (void) setupStreamView:(ControllerSupport*)controllerSupport
swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig; config:(StreamConfiguration*)streamConfig;
- (void) showOnScreenControls; - (void) showOnScreenControls;
- (OnScreenControlsLevel) getCurrentOscState; - (OnScreenControlsLevel) getCurrentOscState;
#if !TARGET_OS_TV
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
#endif
@end @end
File diff suppressed because it is too large Load Diff
+6 -12
View File
@@ -4,8 +4,6 @@
<dict> <dict>
<key>CADisableMinimumFrameDuration</key> <key>CADisableMinimumFrameDuration</key>
<true/> <true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -32,13 +30,13 @@
<key>ProfileName</key> <key>ProfileName</key>
<string>ExtendedGamepad</string> <string>ExtendedGamepad</string>
</dict> </dict>
<dict>
<key>ProfileName</key>
<string>Gamepad</string>
</dict>
</array> </array>
<key>GCSupportsControllerUserInteraction</key> <key>GCSupportsControllerUserInteraction</key>
<true/> <true/>
<key>LSSupportsGameMode</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.games</string> <string>public.app-category.games</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
@@ -48,16 +46,12 @@
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string> <string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_nvstream._tcp</string> <string>_nvstream._tcp</string>
</array> </array>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
+1
View File
@@ -21,4 +21,5 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <CoreData/CoreData.h> #import <CoreData/CoreData.h>
#import "Logger.h" #import "Logger.h"
#include "OSPortabilityDefs.h"
#endif #endif
@@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>Moonlight v1.10.xcdatamodel</string> <string>Moonlight v1.6.xcdatamodel</string>
</dict> </dict>
</plist> </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"?> <?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"> <entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <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="id" attributeType="String" syncable="YES"/>
<attribute name="name" 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"/> <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"> <entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <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="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="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" 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="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="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" 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="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="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useHevc" 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"/> <attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
</entity> </entity>
<elements> <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="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> </elements>
</model> </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>
+2 -2
View File
@@ -16,8 +16,8 @@
self.statusMessage = @"App asset has no status message"; self.statusMessage = @"App asset has no status message";
self.statusCode = -1; self.statusCode = -1;
} }
- (UIImage*) getImage { - (OSImage*) getImage {
return [[UIImage alloc] initWithData:self.data]; return [[OSImage alloc] initWithData:self.data];
} }
@end @end
+1 -1
View File
@@ -20,7 +20,7 @@ static const int MAX_ATTEMPTS = 5;
- (void) main { - (void) main {
int attempts = 0; int attempts = 0;
while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) { 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]; AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]]; [hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
+2 -2
View File
@@ -7,14 +7,14 @@
// //
#import "AppListResponse.h" #import "AppListResponse.h"
#import "TemporaryHost.h"
#ifndef ConnectionHelper_h #ifndef ConnectionHelper_h
#define ConnectionHelper_h #define ConnectionHelper_h
@interface ConnectionHelper : NSObject @interface ConnectionHelper : NSObject
+(AppListResponse*) getAppListForHost:(TemporaryHost*)host; +(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) serverCert uniqueID:(NSString*) uniqueId;
@end @end
+2 -2
View File
@@ -14,8 +14,8 @@
@implementation ConnectionHelper @implementation ConnectionHelper
+(AppListResponse*) getAppListForHost:(TemporaryHost*)host { +(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) cert uniqueID:(NSString*) uniqueId {
HttpManager* hMan = [[HttpManager alloc] initWithHost:host]; HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
// Try up to 5 times to get the app list // Try up to 5 times to get the app list
AppListResponse* appListResp = nil; AppListResponse* appListResp = nil;
+2 -2
View File
@@ -120,7 +120,7 @@
} }
- (ServerInfoResponse*) getServerInfoResponseForAddress:(NSString*)address { - (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]; ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
return serverInfoResponse; return serverInfoResponse;
@@ -202,7 +202,7 @@
callback(host, nil); callback(host, nil);
} }
} else if (!prohibitedAddress) { } 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 { } else {
callback(nil, prohibitedAddressMessage); callback(nil, prohibitedAddressMessage);
} }
+4 -2
View File
@@ -103,8 +103,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert]; ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
receivedResponse = [self checkResponse:serverInfoResp]; receivedResponse = [self checkResponse:serverInfoResp];
if (receivedResponse) { if (receivedResponse) {
_host.activeAddress = address;
[serverInfoResp populateHost:_host]; [serverInfoResp populateHost:_host];
_host.activeAddress = address;
// Update the database using the response // Update the database using the response
DataManager *dataManager = [[DataManager alloc] init]; 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 { - (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]; ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response [hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
withUrlRequest:[hMan newServerInfoRequest:true] withUrlRequest:[hMan newServerInfoRequest:true]
+3 -4
View File
@@ -9,12 +9,10 @@
#import "HttpResponse.h" #import "HttpResponse.h"
#import "HttpRequest.h" #import "HttpRequest.h"
#import "StreamConfiguration.h" #import "StreamConfiguration.h"
#import "TemporaryHost.h"
@interface HttpManager : NSObject <NSURLSessionDelegate> @interface HttpManager : NSObject <NSURLSessionDelegate>
- (id) initWithHost:(TemporaryHost*) host; - (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert;
- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short) httpsPort serverCert:(NSData*) serverCert;
- (void) setServerCert:(NSData*) serverCert; - (void) setServerCert:(NSData*) serverCert;
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert; - (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
- (NSURLRequest*) newUnpairRequest; - (NSURLRequest*) newUnpairRequest;
@@ -26,7 +24,8 @@
- (NSURLRequest*) newServerInfoRequest:(bool)fastFail; - (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail; - (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
- (NSURLRequest*) newHttpServerInfoRequest; - (NSURLRequest*) newHttpServerInfoRequest;
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config; - (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config;
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config;
- (NSURLRequest*) newQuitAppRequest; - (NSURLRequest*) newQuitAppRequest;
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId; - (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
- (void) executeRequestSynchronously:(HttpRequest*)request; - (void) executeRequestSynchronously:(HttpRequest*)request;
+56 -118
View File
@@ -10,7 +10,6 @@
#import "HttpRequest.h" #import "HttpRequest.h"
#import "CryptoManager.h" #import "CryptoManager.h"
#import "TemporaryApp.h" #import "TemporaryApp.h"
#import "ServerInfoResponse.h"
#include <libxml2/libxml/xmlreader.h> #include <libxml2/libxml/xmlreader.h>
#include <string.h> #include <string.h>
@@ -23,16 +22,22 @@
#define EXTRA_LONG_TIMEOUT_SEC 180 #define EXTRA_LONG_TIMEOUT_SEC 180
@implementation HttpManager { @implementation HttpManager {
NSString* _urlSafeHostName; NSURLSession* _urlSession;
NSString* _baseHTTPURL; NSString* _baseHTTPURL;
NSString* _baseHTTPSURL;
NSString* _uniqueId; NSString* _uniqueId;
NSString* _deviceName; NSString* _deviceName;
NSData* _serverCert; NSData* _serverCert;
NSMutableData* _respData;
NSData* _requestResp;
dispatch_semaphore_t _requestLock;
TemporaryHost *_host; // May be nil NSError* _error;
NSString* _baseHTTPSURL;
} }
static const NSString* HTTP_PORT = @"47989";
static const NSString* HTTPS_PORT = @"47984";
+ (NSData*) fixXmlVersion:(NSData*) xmlData { + (NSData*) fixXmlVersion:(NSData*) xmlData {
NSString* dataString = [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding]; NSString* dataString = [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding];
NSString* xmlString = [dataString stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [dataString length])]; NSString* xmlString = [dataString stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [dataString length])];
@@ -44,111 +49,63 @@
_serverCert = serverCert; _serverCert = serverCert;
} }
- (id) initWithHost:(TemporaryHost*) host { - (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert {
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 {
self = [super init]; self = [super init];
// Use the same UID for all Moonlight clients to allow them // Use the same UID for all Moonlight clients to allow them
// quit games started on another Moonlight client. // quit games started on another Moonlight client.
_uniqueId = @"0123456789ABCDEF"; _uniqueId = @"0123456789ABCDEF";
_deviceName = deviceName; _deviceName = deviceName;
_serverCert = serverCert; _serverCert = serverCert;
_requestLock = dispatch_semaphore_create(0);
NSString* address = [Utils addressPortStringToAddress:hostAddressPortString]; _respData = [[NSMutableData alloc] init];
unsigned short port = [Utils addressPortStringToPort:hostAddressPortString]; 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 this is an IPv6 literal, we must properly enclose it in brackets
if ([address containsString:@":"]) { NSString* urlSafeHost;
_urlSafeHostName = [NSString stringWithFormat:@"[%@]", address]; if ([host containsString:@":"]) {
urlSafeHost = [NSString stringWithFormat:@"[%@]", host];
} else { } else {
_urlSafeHostName = address; urlSafeHost = host;
} }
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%u", _urlSafeHostName, port]; _baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", urlSafeHost, HTTP_PORT];
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", urlSafeHost, HTTPS_PORT];
if (httpsPort) {
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, httpsPort];
}
return self; 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 { - (void) executeRequestSynchronously:(HttpRequest*)request {
// This is a special case to handle failure of HTTPS port fetching [_respData setLength:0];
if (!request.request) { _error = nil;
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);
Log(LOG_D, @"Making Request: %@", request); 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) { if (error != NULL) {
Log(LOG_D, @"Connection error: %@", error); Log(LOG_D, @"Connection error: %@", error);
respError = error; self->_error = error;
} }
else { else {
Log(LOG_D, @"Received response: %@", response); Log(LOG_D, @"Received response: %@", response);
if (data != NULL) { if (data != NULL) {
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]); Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
if ([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] != nil) { [self->_respData appendData:data];
requestResp = [HttpManager fixXmlVersion:data]; if ([[NSString alloc] initWithData:self->_respData encoding:NSUTF8StringEncoding] != nil) {
self->_requestResp = [HttpManager fixXmlVersion:self->_respData];
} else { } else {
requestResp = data; self->_requestResp = self->_respData;
} }
} }
} }
dispatch_semaphore_signal(requestLock); dispatch_semaphore_signal(self->_requestLock);
}] resume]; }] resume];
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(requestLock, DISPATCH_TIME_FOREVER); if (!_error && request.response) {
[urlSession invalidateAndCancel]; [request.response populateWithData:_requestResp];
if (!respError && request.response) {
[request.response populateWithData:requestResp];
// If the fallback error code was detected, issue the fallback request // If the fallback error code was detected, issue the fallback request
if (request.response.statusCode == request.fallbackError && request.fallbackRequest != NULL) { if (request.response.statusCode == request.fallbackError && request.fallbackRequest != NULL) {
@@ -159,7 +116,7 @@
[self executeRequestSynchronously:request]; [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 // 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. // a non-matching cert, not because we had no cert at all.
assert(_serverCert != nil); assert(_serverCert != nil);
@@ -174,9 +131,9 @@
[self executeRequestSynchronously:request]; [self executeRequestSynchronously:request];
} }
} }
else if (respError && request.response) { else if (_error && request.response) {
request.response.statusCode = [respError code]; request.response.statusCode = [_error code];
request.response.statusMessage = [respError localizedDescription]; request.response.statusMessage = [_error localizedDescription];
} }
} }
@@ -217,19 +174,11 @@
} }
- (NSURLRequest*) newPairChallenge { - (NSURLRequest*) newPairChallenge {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName]; NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC]; return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
} }
- (NSURLRequest *)newAppListRequest { - (NSURLRequest *)newAppListRequest {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId]; NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC]; return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
} }
@@ -240,10 +189,6 @@
return [self newHttpServerInfoRequest:fastFail]; return [self newHttpServerInfoRequest:fastFail];
} }
if (![self ensureHttpsUrlPopulated:fastFail]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId]; NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)]; return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
} }
@@ -257,49 +202,44 @@
return [self newHttpServerInfoRequest:false]; return [self newHttpServerInfoRequest:false];
} }
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config { - (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
// Using an FPS value over 60 causes SOPS to default to 720p60, // 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 // 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 // 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 // on GFE 3.20.3.
// indicated by a negative version in the last field. int fps = config.frameRate > 60 ? 0 : config.frameRate;
int fps = (config.frameRate > 60 && ![config.appVersion containsString:@".-"]) ? 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", 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, verb, _uniqueId, _baseHTTPSURL, _uniqueId,
config.appID, config.appID,
config.width, config.height, fps, config.width, config.height, fps,
config.optimizeGameSettings ? 1 : 0, config.optimizeGameSettings ? 1 : 0,
[Utils bytesToHex:config.riKey], config.riKeyId, [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, config.playAudioOnPC ? 1 : 0,
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration), SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration),
config.gamepadMask, config.gamepadMask, config.gamepadMask, config.gamepadMask];
!config.multiController ? 1 : 0,
LiGetLaunchUrlQueryParameters()];
Log(LOG_I, @"Requesting: %@", urlString); Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is launching // This blocks while the app is launching
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC]; return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
} }
- (NSURLRequest*) newQuitAppRequest { - (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config {
if (![self ensureHttpsUrlPopulated:NO]) { NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
return nil; _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 {
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId]; NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC]; return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
} }
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId { - (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId]; NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC]; return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
} }
@@ -319,7 +259,7 @@
SecIdentityCopyCertificate(identity, &certificate); SecIdentityCopyCertificate(identity, &certificate);
return [[NSArray alloc] initWithObjects:(__bridge_transfer id)certificate, nil]; return [[NSArray alloc] initWithObjects:(__bridge id)certificate, nil];
} }
// Returns the identity // Returns the identity
@@ -331,14 +271,13 @@
const void *keys[] = { kSecImportExportPassphrase }; const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password }; const void *values[] = { password };
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); 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); OSStatus securityError = SecPKCS12Import(p12Data, options, &items);
if (securityError == errSecSuccess) { if (securityError == errSecSuccess) {
//Log(LOG_D, @"Success opening p12 certificate. Items: %ld", CFArrayGetCount(items)); //Log(LOG_D, @"Success opening p12 certificate. Items: %ld", CFArrayGetCount(items));
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0); CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
identityApp = (SecIdentityRef)CFRetain(CFDictionaryGetValue(identityDict, kSecImportItemIdentity)); identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
CFRelease(items);
} else { } else {
Log(LOG_E, @"Error opening Certificate."); Log(LOG_E, @"Error opening Certificate.");
} }
@@ -392,7 +331,6 @@
SecIdentityRef identity = [self getClientCertificate]; SecIdentityRef identity = [self getClientCertificate];
NSArray* certArray = [self getCertificate:identity]; NSArray* certArray = [self getCertificate:identity];
NSURLCredential* newCredential = [NSURLCredential credentialWithIdentity:identity certificates:certArray persistence:NSURLCredentialPersistencePermanent]; NSURLCredential* newCredential = [NSURLCredential credentialWithIdentity:identity certificates:certArray persistence:NSURLCredentialPersistencePermanent];
CFRelease(identity);
completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential); completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
} }
else else
+9 -14
View File
@@ -72,21 +72,16 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
struct sockaddr* addr = (struct sockaddr*)[addrData bytes]; struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
if (addr->sa_family == AF_INET) { if (addr->sa_family == AF_INET) {
inet_ntop(addr->sa_family, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr)); 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 { else {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr; struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
inet_ntop(addr->sa_family, &sin6->sin6_addr, addrStr, sizeof(addrStr)); 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) { if (sin6->sin6_scope_id != 0) {
// Link-local addresses with scope IDs are special // Link-local addresses with scope IDs are special
return [NSString stringWithFormat: @"[%s%%%u]:%u", addrStr, sin6->sin6_scope_id, port]; return [NSString stringWithFormat: @"%s%%%u", addrStr, sin6->sin6_scope_id];
}
else {
return [NSString stringWithFormat: @"[%s]:%u", addrStr, port];
} }
} }
return [NSString stringWithFormat: @"%s", addrStr];
} }
+ (BOOL)isAddress:(uint8_t*)address inSubnet:(uint8_t*)subnet netmask:(int)bits { + (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 [NSTimer scheduledTimerWithTimeInterval:2.0
target:self target:self
selector:@selector(retryResolveTimerCallback:) selector:@selector(retryResolveTimerCallback:)
userInfo:sender userInfo:nil
repeats:NO]; repeats:NO];
} }
@@ -294,12 +289,12 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
return; return;
} }
NSNetService* service = timer.userInfo; Log(LOG_I, @"Retrying mDNS resolution");
Log(LOG_I, @"Retrying mDNS resolution for %@", service); for (NSNetService* service in services) {
if (service.hostName == nil) {
if (service.hostName == nil) { [service setDelegate:self];
[service setDelegate:self]; [service resolveWithTimeout:5];
[service resolveWithTimeout:5]; }
} }
} }
+8 -12
View File
@@ -38,13 +38,15 @@
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false] [_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]]; fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
if ([serverInfoResp isStatusOk]) { 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"]; NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
if (appversion == nil) { if (appversion == nil) {
[_callback pairFailed:@"Missing XML element"]; [_callback pairFailed:@"Missing XML element"];
return; return;
} }
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue] withState:[serverInfoResp getStringTag:@"state"]]; [self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue]];
} else { } else {
[_callback alreadyPaired]; [_callback alreadyPaired];
} }
@@ -80,8 +82,8 @@
} }
// All codepaths must call finishPairing exactly once before returning! // All codepaths must call finishPairing exactly once before returning!
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion withState:(NSString*)state { - (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion {
Log(LOG_I, @"Pairing with generation %d server in state %@", serverMajorVersion, state); Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
// Start a background task to help prevent the app from being killed // Start a background task to help prevent the app from being killed
// while pairing is in progress. // while pairing is in progress.
@@ -97,13 +99,7 @@
HttpResponse* pairResp = [[HttpResponse alloc] init]; HttpResponse* pairResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]]; [_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
if (![self verifyResponseStatus:pairResp]) { 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. [self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
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."];
}
return; return;
} }
@@ -172,7 +168,7 @@
NSData* serverSecretResp = [Utils hexToBytes:[secretResp getStringTag:@"pairingsecret"]]; NSData* serverSecretResp = [Utils hexToBytes:[secretResp getStringTag:@"pairingsecret"]];
NSData* serverSecret = [serverSecretResp subdataWithRange:NSMakeRange(0, 16)]; 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]]) { if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"]; [self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"];
-4
View File
@@ -11,7 +11,6 @@
#define TAG_HOSTNAME @"hostname" #define TAG_HOSTNAME @"hostname"
#define TAG_EXTERNAL_IP @"ExternalIP" #define TAG_EXTERNAL_IP @"ExternalIP"
#define TAG_HTTPS_PORT @"HttpsPort"
#define TAG_LOCAL_IP @"LocalIP" #define TAG_LOCAL_IP @"LocalIP"
#define TAG_UNIQUE_ID @"uniqueid" #define TAG_UNIQUE_ID @"uniqueid"
#define TAG_MAC_ADDRESS @"mac" #define TAG_MAC_ADDRESS @"mac"
@@ -19,9 +18,6 @@
#define TAG_STATE @"state" #define TAG_STATE @"state"
#define TAG_CURRENT_GAME @"currentgame" #define TAG_CURRENT_GAME @"currentgame"
// Sunshine extension
#define TAG_EXTERNAL_PORT @"ExternalPort"
@interface ServerInfoResponse : HttpResponse <Response> @interface ServerInfoResponse : HttpResponse <Response>
- (void) populateWithData:(NSData *)data; - (void) populateWithData:(NSData *)data;
+2 -46
View File
@@ -23,58 +23,17 @@
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim]; host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] 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 // We might get an IPv4 loopback address if we're using GS IPv6 Forwarder
NSString *lanAddr = [[self getStringTag:TAG_LOCAL_IP] trim]; NSString *lanAddr = [[self getStringTag:TAG_LOCAL_IP] trim];
if (![lanAddr hasPrefix:@"127."]) { if (![lanAddr hasPrefix:@"127."]) {
unsigned short localPort; host.localAddress = lanAddr;
// 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;
}
} }
// Modern GFE versions don't actually give us a WAN address anymore // 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. // so we leave the one that we populated from mDNS discovery via STUN.
NSString *wanAddr = [[self getStringTag:TAG_EXTERNAL_IP] trim]; NSString *wanAddr = [[self getStringTag:TAG_EXTERNAL_IP] trim];
if (wanAddr) { if (wanAddr) {
host.externalAddress = [Utils addressAndPortToAddressPortString:wanAddr port:externalHttpPort]; host.externalAddress = wanAddr;
}
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];
} }
NSString *state = [[self getStringTag:TAG_STATE] trim]; NSString *state = [[self getStringTag:TAG_STATE] trim];
@@ -85,9 +44,6 @@
host.currentGame = @"0"; host.currentGame = @"0";
} }
// GFE uses the Mjolnir codename in their state enum values
host.isNvidiaServerSoftware = [state containsString:@"MJOLNIR"];
NSInteger pairStatus; NSInteger pairStatus;
if ([self getIntTag:TAG_PAIR_STATUS value:&pairStatus]) { if ([self getIntTag:TAG_PAIR_STATUS value:&pairStatus]) {
host.pairState = pairStatus ? PairStatePaired : PairStateUnpaired; host.pairState = pairStatus ? PairStatePaired : PairStateUnpaired;
+13 -34
View File
@@ -16,15 +16,12 @@
@implementation WakeOnLanManager @implementation WakeOnLanManager
static const int numStaticPorts = 2; static const int numPorts = 7;
static const int staticPorts[numStaticPorts] = { static const int ports[numPorts] = {
9, // Standard WOL port (privileged port) 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) 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 { + (void) populateAddress:(struct sockaddr_storage*)addr withPort:(unsigned short)port {
if (addr->ss_family == AF_INET) { if (addr->ss_family == AF_INET) {
@@ -41,33 +38,29 @@ static const int dynamicPorts[numDynamicPorts] = {
NSData* wolPayload = [WakeOnLanManager createPayload:host]; NSData* wolPayload = [WakeOnLanManager createPayload:host];
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
NSString* address; const char* address;
struct addrinfo hints, *res, *curr; struct addrinfo hints, *res, *curr;
// try all ip addresses // try all ip addresses
if (i == 0 && host.localAddress != nil) { if (i == 0 && host.localAddress != nil) {
address = host.localAddress; address = [host.localAddress UTF8String];
} else if (i == 1 && host.externalAddress != nil) { } else if (i == 1 && host.externalAddress != nil) {
address = host.externalAddress; address = [host.externalAddress UTF8String];
} else if (i == 2 && host.address != nil) { } else if (i == 2 && host.address != nil) {
address = host.address; address = [host.address UTF8String];
} else if (i == 3 && host.ipv6Address != nil) { } else if (i == 3 && host.ipv6Address != nil) {
address = host.ipv6Address; address = [host.ipv6Address UTF8String];
} else if (i == 4) { } else if (i == 4) {
address = @"255.255.255.255"; address = "255.255.255.255";
} else { } else {
// Requested address wasn't present // Requested address wasn't present
continue; 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)); memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; hints.ai_family = AF_UNSPEC;
hints.ai_flags = AI_ADDRCONFIG; 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 // Failed to resolve address
Log(LOG_E, @"Failed to resolve WOL address"); Log(LOG_E, @"Failed to resolve WOL address");
continue; continue;
@@ -93,29 +86,15 @@ static const int dynamicPorts[numDynamicPorts] = {
memset(&addr, 0, sizeof(addr)); memset(&addr, 0, sizeof(addr));
memcpy(&addr, curr->ai_addr, curr->ai_addrlen); memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
for (int j = 0; j < numStaticPorts; j++) { for (int j = 0; j < numPorts; j++) {
[WakeOnLanManager populateAddress:&addr withPort:staticPorts[j]]; [WakeOnLanManager populateAddress:&addr withPort:ports[j]];
long err = sendto(wolSocket, long err = sendto(wolSocket,
[wolPayload bytes], [wolPayload bytes],
[wolPayload length], [wolPayload length],
0, 0,
(struct sockaddr*)&addr, (struct sockaddr*)&addr,
curr->ai_addrlen); curr->ai_addrlen);
Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", staticPorts[j], err); Log(LOG_I, @"Sending WOL packet returned: %ld", 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);
} }
close(wolSocket); close(wolSocket);
+24
View File
@@ -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 */
+12 -13
View File
@@ -11,24 +11,23 @@
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org" #define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
typedef struct { @protocol ConnectionCallbacks <NSObject>
CFTimeInterval startTime;
CFTimeInterval endTime; - (void) connectionStarted;
int totalFrames; - (void) connectionTerminated:(int)errorCode;
int receivedFrames; - (void) stageStarting:(const char*)stageName;
int networkDroppedFrames; - (void) stageComplete:(const char*)stageName;
int totalHostProcessingLatency; - (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags;
int framesWithHostProcessingLatency; - (void) launchFailed:(NSString*)message;
int maxHostProcessingLatency; - (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
int minHostProcessingLatency; - (void) connectionStatusUpdate:(int)status;
} video_stats_t;
@end
@interface Connection : NSOperation <NSStreamDelegate> @interface Connection : NSOperation <NSStreamDelegate>
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks; -(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
-(void) terminate; -(void) terminate;
-(void) main; -(void) main;
-(BOOL) getVideoStats:(video_stats_t*)stats;
-(NSString*) getActiveCodecName;
@end @end
+204 -245
View File
@@ -9,11 +9,10 @@
#import "Connection.h" #import "Connection.h"
#import "Utils.h" #import "Utils.h"
#import <AudioUnit/AudioUnit.h>
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h> #import <VideoToolbox/VideoToolbox.h>
#define SDL_MAIN_HANDLED
#import <SDL.h>
#include "Limelight.h" #include "Limelight.h"
#include "opus_multistream.h" #include "opus_multistream.h"
@@ -26,107 +25,41 @@
char _hostString[256]; char _hostString[256];
char _appVersionString[32]; char _appVersionString[32];
char _gfeVersionString[32]; char _gfeVersionString[32];
char _rtspSessionUrl[128];
} }
static NSLock* initLock; static NSLock* initLock;
static OpusMSDecoder* opusDecoder; static OpusMSDecoder* opusDecoder;
static id<ConnectionCallbacks> _callbacks; 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; #define OUTPUT_BUS 0
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
static void* audioBuffer;
static int audioFrameSize;
// 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; static VideoDecoderRenderer* renderer;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags) int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
{ {
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate]; [renderer setupWithVideoFormat:videoFormat refreshRate:redrawRate];
lastFrameNumber = 0;
activeVideoFormat = videoFormat;
memset(&currentVideoStats, 0, sizeof(currentVideoStats));
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
return 0; return 0;
} }
void DrStart(void) void DrCleanup(void)
{ {
[renderer start]; [renderer cleanup];
}
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";
}
} }
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit) int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
@@ -139,46 +72,6 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
return DR_NEED_IDR; 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(&currentVideoStats, 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; PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) { while (entry != NULL) {
// Submit parameter set NALUs directly since no copy is required by the decoder // Submit parameter set NALUs directly since no copy is required by the decoder
@@ -186,7 +79,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
length:entry->length length:entry->length
bufferType:entry->bufferType bufferType:entry->bufferType
decodeUnit:decodeUnit]; pts:decodeUnit->presentationTimeMs];
if (ret != DR_OK) { if (ret != DR_OK) {
free(data); free(data);
return ret; return ret;
@@ -204,60 +97,116 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
return [renderer submitDecodeBuffer:data return [renderer submitDecodeBuffer:data
length:offset length:offset
bufferType:BUFFER_TYPE_PICDATA 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; int err;
SDL_AudioSpec want, have; AudioChannelLayout channelLayout = {};
OPUS_MULTISTREAM_CONFIGURATION opusConfig = *originalOpusConfig;
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { // Initialize the circular buffer
Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError()); 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; return -1;
} }
SDL_zero(want); switch (opusConfig.channelCount) {
want.freq = opusConfig->sampleRate; case 2:
want.format = AUDIO_S16; channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
want.channels = opusConfig->channelCount; break;
want.samples = opusConfig->samplesPerFrame; case 4:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic;
break;
case 6:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_5_1;
break;
case 8:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_7_1;
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); // Swap SL/SR and RL/RR to match the selected channel layout
if (audioDevice == 0) { opusConfig.mapping[4] = originalOpusConfig->mapping[6];
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError()); opusConfig.mapping[5] = originalOpusConfig->mapping[7];
ArCleanup(); opusConfig.mapping[6] = originalOpusConfig->mapping[4];
return -1; opusConfig.mapping[7] = originalOpusConfig->mapping[5];
break;
default:
// Unsupported channel layout
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig.channelCount);
abort();
} }
audioConfig = *opusConfig; opusDecoder = opus_multistream_decoder_create(opusConfig.sampleRate,
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount; opusConfig.channelCount,
audioBuffer = SDL_malloc(audioFrameSize); opusConfig.streams,
if (audioBuffer == NULL) { opusConfig.coupledStreams,
Log(LOG_E, @"Failed to allocate audio frame buffer"); opusConfig.mapping,
ArCleanup();
return -1;
}
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&err); &err);
if (opusDecoder == NULL) {
Log(LOG_E, @"Failed to create Opus decoder"); // Configure the audio session for our app
ArCleanup(); NSError *audioSessionError = nil;
return -1; 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 // We need to specify a channel layout for surround sound configurations
SDL_PauseAudioDevice(audioDevice, 0); 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) for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * opusConfig.samplesPerFrame, &audioBuffers[i]);
if (status != noErr) {
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
return status;
}
return 0; FillOutputBuffer(nil, audioQueue, audioBuffers[i]);
}
status = AudioQueueStart(audioQueue, nil);
if (status != noErr) {
Log(LOG_E, @"Error starting queue: %d\n", status);
return status;
}
return status;
} }
void ArCleanup(void) void ArCleanup(void)
@@ -267,43 +216,43 @@ void ArCleanup(void)
opusDecoder = NULL; opusDecoder = NULL;
} }
if (audioDevice != 0) { // Stop before disposing to avoid massive delay inside
SDL_CloseAudioDevice(audioDevice); // AudioQueueDispose() (iOS bug?)
audioDevice = 0; 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) { // Audio session is now inactive
SDL_free(audioBuffer); [[AVAudioSession sharedInstance] setActive: NO error: nil];
audioBuffer = NULL;
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
} }
void ArDecodeAndPlaySample(char* sampleData, int sampleLength) void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
{ {
int decodeLen; int decodeLen;
// Don't queue if there's already more than 30 ms of audio data waiting // Check if there is space for this sample in the buffer. Again, this can race
// in Moonlight's audio queue. // but in the worst case, we'll not see the sample callback having consumed a sample.
if (LiGetPendingAudioDuration() > 30) { if (((audioBufferWriteIndex + 1) % audioBufferEntries) == audioBufferReadIndex) {
return; return;
} }
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength, decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
(short*)audioBuffer, audioConfig.samplesPerFrame, 0); (short*)&audioCircularBuffer[audioBufferWriteIndex * audioBufferStride], audioSamplesPerFrame, 0);
if (decodeLen > 0) { if (decodeLen > 0) {
// Provide backpressure on the queue to ensure too many frames don't build up // Use a full memory barrier to ensure the circular buffer is written before incrementing the index
// in SDL's audio queue. __sync_synchronize();
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
SDL_Delay(1);
}
if (SDL_QueueAudio(audioDevice, // This can race with the reader in the sample callback, however this is a benign
audioBuffer, // race since we'll either read the original value of s_WriteIndex (which is safe,
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) { // we just won't consider this sample) or the new value of s_WriteIndex
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError()); audioBufferWriteIndex = (audioBufferWriteIndex + 1) % audioBufferEntries;
}
} }
} }
@@ -350,27 +299,6 @@ void ClConnectionStatusUpdate(int status)
[_callbacks connectionStatusUpdate: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 -(void) terminate
{ {
// Interrupt any action blocking LiStartConnection(). This is // 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]; initLock = [[NSLock alloc] init];
} }
if (videoStatsLock == nil) {
videoStatsLock = [[NSLock alloc] init];
}
NSString *rawAddress = [Utils addressPortStringToAddress:config.host];
strncpy(_hostString, strncpy(_hostString,
[rawAddress cStringUsingEncoding:NSUTF8StringEncoding], [config.host cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_hostString) - 1); sizeof(_hostString));
strncpy(_appVersionString, strncpy(_appVersionString,
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding], [config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_appVersionString) - 1); sizeof(_appVersionString));
if (config.gfeVersion != nil) { if (config.gfeVersion != nil) {
strncpy(_gfeVersionString, strncpy(_gfeVersionString,
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding], [config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_gfeVersionString) - 1); sizeof(_gfeVersionString));
}
if (config.rtspSessionUrl != nil) {
strncpy(_rtspSessionUrl,
[config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_rtspSessionUrl) - 1);
} }
LiInitializeServerInformation(&_serverInfo); LiInitializeServerInformation(&_serverInfo);
@@ -427,10 +345,6 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
if (config.gfeVersion != nil) { if (config.gfeVersion != nil) {
_serverInfo.serverInfoGfeVersion = _gfeVersionString; _serverInfo.serverInfoGfeVersion = _gfeVersionString;
} }
if (config.rtspSessionUrl != nil) {
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
}
_serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
renderer = myRenderer; renderer = myRenderer;
_callbacks = callbacks; _callbacks = callbacks;
@@ -440,13 +354,13 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_streamConfig.height = config.height; _streamConfig.height = config.height;
_streamConfig.fps = config.frameRate; _streamConfig.fps = config.frameRate;
_streamConfig.bitrate = config.bitRate; _streamConfig.bitrate = config.bitRate;
_streamConfig.supportedVideoFormats = config.supportedVideoFormats; _streamConfig.enableHdr = config.enableHdr;
_streamConfig.audioConfiguration = config.audioConfiguration; _streamConfig.audioConfiguration = config.audioConfiguration;
// Since we require iOS 12 or above, we're guaranteed to be running // Use some of the HEVC encoding efficiency improvements to
// on a 64-bit device with ARMv8 crypto instructions, so we don't // reduce bandwidth usage while still gaining some image
// need to check for that here. // quality improvement.
_streamConfig.encryptionFlags = ENCFLG_ALL; _streamConfig.hevcBitratePercentageMultiplier = 75;
if ([Utils isActiveNetworkVPN]) { if ([Utils isActiveNetworkVPN]) {
// Force remote streaming mode when a VPN is connected // Force remote streaming mode when a VPN is connected
@@ -459,6 +373,23 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_streamConfig.packetSize = 1392; _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]); memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
memset(_streamConfig.remoteInputAesIv, 0, 16); memset(_streamConfig.remoteInputAesIv, 0, 16);
int riKeyId = htonl(config.riKeyId); int riKeyId = htonl(config.riKeyId);
@@ -466,17 +397,20 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
LiInitializeVideoCallbacks(&_drCallbacks); LiInitializeVideoCallbacks(&_drCallbacks);
_drCallbacks.setup = DrDecoderSetup; _drCallbacks.setup = DrDecoderSetup;
_drCallbacks.start = DrStart; _drCallbacks.cleanup = DrCleanup;
_drCallbacks.stop = DrStop; _drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC | // RFI doesn't work properly with HEVC on iOS 11 with an iPhone SE (at least)
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1; // It doesnt work on macOS either, tested with Network Link Conditioner.
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
CAPABILITY_DIRECT_SUBMIT;
LiInitializeAudioCallbacks(&_arCallbacks); LiInitializeAudioCallbacks(&_arCallbacks);
_arCallbacks.init = ArInit; _arCallbacks.init = ArInit;
_arCallbacks.cleanup = ArCleanup; _arCallbacks.cleanup = ArCleanup;
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample; _arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION; _arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
LiInitializeConnectionCallbacks(&_clCallbacks); LiInitializeConnectionCallbacks(&_clCallbacks);
_clCallbacks.stageStarting = ClStageStarting; _clCallbacks.stageStarting = ClStageStarting;
@@ -487,14 +421,39 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_clCallbacks.logMessage = ClLogMessage; _clCallbacks.logMessage = ClLogMessage;
_clCallbacks.rumble = ClRumble; _clCallbacks.rumble = ClRumble;
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate; _clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
_clCallbacks.setHdrMode = ClSetHdrMode;
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
_clCallbacks.setMotionEventState = ClSetMotionEventState;
_clCallbacks.setControllerLED = ClSetControllerLED;
return self; 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 -(void) main
{ {
[initLock lock]; [initLock lock];
-25
View File
@@ -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
+3 -6
View File
@@ -9,27 +9,24 @@
@interface StreamConfiguration : NSObject @interface StreamConfiguration : NSObject
@property NSString* host; @property NSString* host;
@property unsigned short httpsPort;
@property NSString* appVersion; @property NSString* appVersion;
@property NSString* gfeVersion; @property NSString* gfeVersion;
@property NSString* appID; @property NSString* appID;
@property NSString* appName; @property NSString* appName;
@property NSString* rtspSessionUrl;
@property int serverCodecModeSupport;
@property int width; @property int width;
@property int height; @property int height;
@property int frameRate; @property int frameRate;
@property int bitRate; @property int bitRate;
@property int riKeyId; @property int riKeyId;
@property BOOL streamingRemotely;
@property NSData* riKey; @property NSData* riKey;
@property int gamepadMask; @property int gamepadMask;
@property BOOL optimizeGameSettings; @property BOOL optimizeGameSettings;
@property BOOL playAudioOnPC; @property BOOL playAudioOnPC;
@property BOOL swapABXYButtons;
@property int audioConfiguration; @property int audioConfiguration;
@property int supportedVideoFormats; @property BOOL enableHdr;
@property BOOL multiController; @property BOOL multiController;
@property BOOL useFramePacing; @property BOOL allowHevc;
@property NSData* serverCert; @property NSData* serverCert;
@end @end
+1 -1
View File
@@ -9,5 +9,5 @@
#import "StreamConfiguration.h" #import "StreamConfiguration.h"
@implementation StreamConfiguration @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 @end
+1 -3
View File
@@ -11,10 +11,8 @@
@interface StreamManager : NSOperation @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; - (void) stopStream;
- (NSString*) getStatsOverlayText;
@end @end
+19 -78
View File
@@ -17,17 +17,15 @@
#import "HttpRequest.h" #import "HttpRequest.h"
#import "IdManager.h" #import "IdManager.h"
#include <Limelight.h>
@implementation StreamManager { @implementation StreamManager {
StreamConfiguration* _config; StreamConfiguration* _config;
UIView* _renderView; OSView* _renderView;
id<ConnectionCallbacks> _callbacks; id<ConnectionCallbacks> _callbacks;
Connection* _connection; 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]; self = [super init];
_config = config; _config = config;
_renderView = view; _renderView = view;
@@ -39,8 +37,10 @@
- (void)main { - (void)main {
[CryptoManager generateKeyPairUsingSSL]; [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]; serverCert:_config.serverCert];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init]; ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
@@ -65,13 +65,15 @@
return; return;
} }
// Only perform this check on GFE (as indicated by MJOLNIR in state value) // resumeApp and launchApp handle calling launchFailed
if ((_config.width > 4096 || _config.height > 4096) && [serverState containsString:@"MJOLNIR"]) { if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
// Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K. // App already running, resume it
// We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation. if (![self resumeApp:hMan]) {
NSString* codecSupport = [serverInfoResp getStringTag:@"ServerCodecModeSupport"]; return;
if (codecSupport == nil || !([codecSupport intValue] & 0x200)) { }
[_callbacks launchFailed:@"Your host PC's GPU doesn't support streaming video resolutions over 4K."]; } else {
// Start app
if (![self launchApp:hMan]) {
return; return;
} }
} }
@@ -80,26 +82,9 @@
_config.appVersion = appversion; _config.appVersion = appversion;
_config.gfeVersion = gfeVersion; _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 // Initializing the renderer must be done on the main thread
dispatch_async(dispatch_get_main_queue(), ^{ 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]; self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init]; NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection]; [opQueue addOperation:self->_connection];
@@ -111,9 +96,9 @@
[_connection terminate]; [_connection terminate];
} }
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl { - (BOOL) launchApp:(HttpManager*)hMan {
HttpResponse* launchResp = [[HttpResponse alloc] init]; 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"]; NSString *gameSession = [launchResp getStringTag:@"gamesession"];
if (![launchResp isStatusOk]) { if (![launchResp isStatusOk]) {
[_callbacks launchFailed:launchResp.statusMessage]; [_callbacks launchFailed:launchResp.statusMessage];
@@ -125,13 +110,12 @@
return FALSE; return FALSE;
} }
*sessionUrl = [launchResp getStringTag:@"sessionUrl0"];
return TRUE; return TRUE;
} }
- (BOOL) resumeApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl { - (BOOL) resumeApp:(HttpManager*)hMan {
HttpResponse* resumeResp = [[HttpResponse alloc] init]; 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"]; NSString* resume = [resumeResp getStringTag:@"resume"];
if (![resumeResp isStatusOk]) { if (![resumeResp isStatusOk]) {
[_callbacks launchFailed:resumeResp.statusMessage]; [_callbacks launchFailed:resumeResp.statusMessage];
@@ -143,50 +127,7 @@
return FALSE; return FALSE;
} }
*sessionUrl = [resumeResp getStringTag:@"sessionUrl0"];
return TRUE; 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 @end
+7 -10
View File
@@ -8,19 +8,16 @@
@import AVFoundation; @import AVFoundation;
#import "ConnectionCallbacks.h"
#include "Limelight.h"
@interface VideoDecoderRenderer : NSObject @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)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate;
- (void)start;
- (void)stop;
- (void)setHdrMode:(BOOL)enabled;
- (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 @end
+233 -533
View File
@@ -9,32 +9,19 @@
#import "VideoDecoderRenderer.h" #import "VideoDecoderRenderer.h"
#import "StreamView.h" #import "StreamView.h"
#include <libavcodec/avcodec.h> #include "Limelight.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);
@implementation VideoDecoderRenderer { @implementation VideoDecoderRenderer {
StreamView* _view; StreamView* _view;
id<ConnectionCallbacks> _callbacks;
float _streamAspectRatio;
AVSampleBufferDisplayLayer* displayLayer; AVSampleBufferDisplayLayer* displayLayer;
Boolean waitingForSps, waitingForPps, waitingForVps;
int videoFormat; int videoFormat;
int frameRate;
NSMutableArray *parameterSetBuffers; NSData *spsData, *ppsData, *vpsData;
NSData *masteringDisplayColorVolume;
NSData *contentLightLevelInfo;
CMVideoFormatDescriptionRef formatDesc; CMVideoFormatDescriptionRef formatDesc;
CADisplayLink* _displayLink; CADisplayLink* _displayLink;
BOOL framePacing;
} }
- (void)reinitializeDisplayLayer - (void)reinitializeDisplayLayer
@@ -42,22 +29,11 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
CALayer *oldLayer = displayLayer; CALayer *oldLayer = displayLayer;
displayLayer = [[AVSampleBufferDisplayLayer alloc] init]; 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.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height); displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
displayLayer.videoGravity = AVLayerVideoGravityResize;
// Hide the layer until we get an IDR frame. This ensures we // Hide the layer until we get an IDR frame. This ensures we
// can see the loading progress label as the stream is starting. // can see the loading progress label as the stream is starting.
@@ -71,442 +47,236 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
[_view.layer addSublayer:displayLayer]; [_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) { if (formatDesc != nil) {
CFRelease(formatDesc); CFRelease(formatDesc);
formatDesc = nil; formatDesc = nil;
} }
} }
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing - (id)initWithView:(StreamView*)view
{ {
self = [super init]; self = [super init];
_view = view; _view = view;
_callbacks = callbacks;
_streamAspectRatio = aspectRatio;
framePacing = useFramePacing;
parameterSetBuffers = [[NSMutableArray alloc] init];
[self reinitializeDisplayLayer]; [self reinitializeDisplayLayer];
return self; 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->videoFormat = videoFormat;
self->frameRate = frameRate;
}
- (void)start if (refreshRate > 60) {
{ // HACK: We seem to just get 60 Hz screen updates even with a 120 FPS stream if
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)]; // we don't set preferredFramesPerSecond somewhere. Since we're a UIKit view, we
if (@available(iOS 15.0, tvOS 15.0, *)) { // have to use CADisplayLink for that. See https://github.com/moonlight-stream/moonlight-ios/issues/372
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->frameRate, self->frameRate, self->frameRate); _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 - (void)displayLinkCallback:(CADisplayLink *)sender
{ {
VIDEO_FRAME_HANDLE handle; // No-op - rendering done in submitDecodeBuffer
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;
}
}
}
}
} }
- (void)stop - (void)cleanup
{ {
[_displayLink invalidate]; [_displayLink invalidate];
} }
#define FRAME_START_PREFIX_SIZE 4
#define NALU_START_PREFIX_SIZE 3 #define NALU_START_PREFIX_SIZE 3
#define NAL_LENGTH_PREFIX_SIZE 4 #define NAL_LENGTH_PREFIX_SIZE 4
- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength - (Boolean)readyForPictureData
{ {
OSStatus status; if (videoFormat & VIDEO_FORMAT_MASK_H264) {
size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer); return !waitingForSps && !waitingForPps;
// 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];
} }
else { 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 - (Boolean)isNalReferencePicture:(unsigned char)nalType
- (CMVideoFormatDescriptionRef)createAV1FormatDescriptionForIDRFrame:(NSData*)frameData { {
NSMutableDictionary* extensions = [[NSMutableDictionary alloc] init]; if (videoFormat & VIDEO_FORMAT_MASK_H264) {
return nalType == 0x65;
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;
} }
else {
AVPacket avPacket = {}; // HEVC has several types of reference NALU types
avPacket.data = (uint8_t*)frameData.bytes; switch (nalType) {
avPacket.size = (int)frameData.length; case 0x20:
case 0x22:
// Read the sequence header OBU case 0x24:
CodedBitstreamFragment cbsFrag = {}; case 0x26:
err = ff_cbs_read_packet(cbsCtx, &cbsFrag, &avPacket); case 0x28:
if (err < 0) { case 0x2A:
Log(LOG_E, @"ff_cbs_read_packet() failed: %d", err); return true;
ff_cbs_close(&cbsCtx); default:
return nil; return false;
}
} }
}
#define SET_CFSTR_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (__bridge NSString*)(value) - (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
#define SET_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (value) {
OSStatus status;
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
SET_EXTENSION(kCMFormatDescriptionExtension_FormatName, @"av01"); // 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;
// We use the value for YUV without alpha, same as Chrome // Pass the real buffer pointer directly (no offset)
// https://developer.apple.com/library/archive/qa/qa1183/_index.html // This will give it to the block buffer to free when it's released.
SET_EXTENSION(kCMFormatDescriptionExtension_Depth, @24); // 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;
}
CodedBitstreamAV1Context* bitstreamCtx = (CodedBitstreamAV1Context*)cbsCtx->priv_data; // Write the length prefix to existing buffer
AV1RawSequenceHeader* seqHeader = bitstreamCtx->sequence_header; const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
if (seqHeader == NULL) { (uint8_t)(dataLength >> 8), (uint8_t)dataLength};
Log(LOG_E, @"AV1 sequence header not found in IDR frame!"); status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
ff_cbs_fragment_free(&cbsFrag); oldOffset, NAL_LENGTH_PREFIX_SIZE);
ff_cbs_close(&cbsCtx); if (status != noErr) {
return nil; 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;
}
} }
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 // 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; OSStatus status;
// Construct a new format description object each time we receive an IDR frame if (bufferType != BUFFER_TYPE_PICDATA) {
if (du->frameType == FRAME_TYPE_IDR) { if (bufferType == BUFFER_TYPE_VPS) {
if (bufferType != BUFFER_TYPE_PICDATA) { Log(LOG_I, @"Got VPS");
if (bufferType == BUFFER_TYPE_VPS || bufferType == BUFFER_TYPE_SPS || bufferType == BUFFER_TYPE_PPS) { vpsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
// Add new parameter set into the parameter set array waitingForVps = false;
int startLen = data[2] == 0x01 ? 3 : 4;
[parameterSetBuffers addObject:[NSData dataWithBytes:&data[startLen] length:length - startLen]];
}
// 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 // We got a new SPS so wait for a new PPS to match it
return DR_OK; 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. // See if we've got all the parameter sets we need for our video format
// This is the only way we know that there is no more CSD for this frame. if ([self readyForPictureData]) {
// if (videoFormat & VIDEO_FORMAT_MASK_H264) {
// NB: This logic depends on the fact that we submit all picture data in one buffer! const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
// Free the old format description Log(LOG_I, @"Constructing new H264 format description");
if (formatDesc != NULL) { status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
CFRelease(formatDesc); 2, /* count of parameter sets */
formatDesc = NULL; 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) { // Data is NOT to be freed here. It's a direct usage of the caller's buffer.
// 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"); // No frame data to submit for these NALUs
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, return DR_OK;
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();
}
} }
if (formatDesc == NULL) { if (formatDesc == NULL) {
@@ -519,9 +289,13 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) { if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error); Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
// Recreate the display layer. We are already on the main thread, // Recreate the display layer on the main thread.
// so this is safe to do right here. // We need to use dispatch_sync() or we may miss
[self reinitializeDisplayLayer]; // 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 // Request an IDR frame to initialize the new decoder
free(data); free(data);
@@ -529,156 +303,82 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
} }
// Now we're decoding actual frame data here // Now we're decoding actual frame data here
CMBlockBufferRef frameBlockBuffer; CMBlockBufferRef blockBuffer;
CMBlockBufferRef dataBlockBuffer;
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorDefault, NULL, 0, length, 0, &dataBlockBuffer); status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
if (status != noErr) { if (status != noErr) {
Log(LOG_E, @"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status); Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
free(data); free(data);
return DR_NEED_IDR; return DR_NEED_IDR;
} }
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced int lastOffset = -1;
for (int i = 0; i < length - FRAME_START_PREFIX_SIZE; i++) {
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &frameBlockBuffer); // Search for a NALU
if (status != noErr) { if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status); // It's the start of a new NALU
CFRelease(dataBlockBuffer); if (lastOffset != -1) {
return DR_NEED_IDR; // We've seen a start before this so enqueue that NALU
} [self updateBufferForRange:blockBuffer data:data offset:lastOffset length:i - lastOffset];
// 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) { lastOffset = i;
// 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 if (lastOffset != -1) {
status = CMBlockBufferAppendBufferReference(frameBlockBuffer, dataBlockBuffer, 0, length, 0); // Enqueue the remaining data
if (status != noErr) { [self updateBufferForRange:blockBuffer data:data offset:lastOffset length:length - lastOffset];
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
return DR_NEED_IDR;
}
} }
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
CMSampleBufferRef sampleBuffer; CMSampleBufferRef sampleBuffer;
CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeUs, 1000000), kCMTimeInvalid}; status = CMSampleBufferCreate(kCFAllocatorDefault,
blockBuffer,
status = CMSampleBufferCreateReady(kCFAllocatorDefault, true, NULL,
frameBlockBuffer, NULL, formatDesc, 1, 0,
formatDesc, 1, 1, NULL, 0, NULL,
&sampleTiming, 0, NULL,
&sampleBuffer); &sampleBuffer);
if (status != noErr) { if (status != noErr) {
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status); Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
CFRelease(dataBlockBuffer); CFRelease(blockBuffer);
CFRelease(frameBlockBuffer);
return DR_NEED_IDR; return DR_NEED_IDR;
} }
// Enqueue the next frame CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
[self->displayLayer enqueueSampleBuffer:sampleBuffer]; CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
if (du->frameType == FRAME_TYPE_IDR) { CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
// Ensure the layer is visible now CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
self->displayLayer.hidden = NO;
// Tell our parent VC to hide the progress indicator if (![self isNalReferencePicture:nalType]) {
[self->_callbacks videoContentShown]; // 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 // Enqueue video samples on the main thread
CFRelease(dataBlockBuffer); dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(frameBlockBuffer); // Enqueue the next frame
CFRelease(sampleBuffer); [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; 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 @end
+1 -6
View File
@@ -11,16 +11,11 @@
@protocol AppCallback <NSObject> @protocol AppCallback <NSObject>
- (void) appClicked:(TemporaryApp*)app view:(UIView*)view; - (void) appClicked:(TemporaryApp*) app;
- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
@end @end
#if !TARGET_OS_TV
@interface UIAppView : UIButton <UIContextMenuInteractionDelegate>
#else
@interface UIAppView : UIButton @interface UIAppView : UIButton
#endif
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback; - (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
- (void) updateAppImage; - (void) updateAppImage;
+16 -59
View File
@@ -9,7 +9,7 @@
#import "UIAppView.h" #import "UIAppView.h"
#import "AppAssetManager.h" #import "AppAssetManager.h"
static const float REFRESH_CYCLE = 1.0f; static const float REFRESH_CYCLE = 2.0f;
@implementation UIAppView { @implementation UIAppView {
TemporaryApp* _app; TemporaryApp* _app;
@@ -40,28 +40,17 @@ static UIImage* noImage;
self.frame = CGRectMake(0, 0, 150, 200); self.frame = CGRectMake(0, 0, 150, 200);
#endif #endif
[self setAlpha:app.hidden ? 0.4 : 1.0];
_appImage = [[UIImageView alloc] initWithFrame:self.frame]; _appImage = [[UIImageView alloc] initWithFrame:self.frame];
[_appImage setImage:noImage]; [_appImage setImage:noImage];
[self addSubview:_appImage]; [self addSubview:_appImage];
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer if (@available(iOS 9.0, tvOS 9.0, *)) {
// for tvOS devices and iOS prior to 13.0. [self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
#if !TARGET_OS_TV
if (@available(iOS 13.0, *)) {
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[self addInteraction:rightClickInteraction];
} }
else else {
#endif [self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
{
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(appLongClicked:)];
[self addGestureRecognizer:longPressRecognizer];
} }
[self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown]; [self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit]; [self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
@@ -80,40 +69,15 @@ static UIImage* noImage;
#endif #endif
[self updateAppImage]; [self updateAppImage];
[self startUpdateLoop];
return self; return self;
} }
- (void)didMoveToSuperview { - (void) appClicked {
// Start our update loop when we are added to our cell [_callback appClicked:_app];
if (self.superview != nil) {
[self updateLoop];
}
} }
- (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 { - (void) updateAppImage {
if (_appOverlay != nil) { if (_appOverlay != nil) {
[_appOverlay removeFromSuperview]; [_appOverlay removeFromSuperview];
@@ -210,28 +174,21 @@ static UIImage* noImage;
} }
} }
- (void) updateLoop { - (void) startUpdateLoop {
// Stop immediately if the view has been detached [self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
if (self.superview == nil) { }
return;
}
- (void) updateLoop {
// Update the app image if neccessary // Update the app image if neccessary
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) || if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) { (_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
[self updateAppImage]; [self updateAppImage];
} }
// Show no shadow for hidden apps. Because we adjust the opacity of the // Stop updating when we detach from our parent view
// cells for hidden apps, it makes them look bad when the shadow draws if (self.superview != nil) {
// through the app tile. [self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
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];
} }
@end @end
-4
View File
@@ -17,11 +17,7 @@
@end @end
#if !TARGET_OS_TV
@interface UIComputerView : UIButton <UIContextMenuInteractionDelegate>
#else
@interface UIComputerView : UIButton @interface UIComputerView : UIButton
#endif
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback; - (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback;
- (id) initForAddWithCallback:(id<HostCallback>)callback; - (id) initForAddWithCallback:(id<HostCallback>)callback;
+23 -42
View File
@@ -102,7 +102,12 @@ static const int LABEL_DY = 20;
self = [self init]; self = [self init];
_callback = callback; _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 setText:@"Add Host Manually"];
[_hostLabel sizeToFit]; [_hostLabel sizeToFit];
@@ -119,34 +124,22 @@ static const int LABEL_DY = 20;
_host = host; _host = host;
_callback = callback; _callback = callback;
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
// for tvOS devices and iOS prior to 13.0. [self addGestureRecognizer:longPressRecognizer];
#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];
}
[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 updateContentsForHost:host];
[self startUpdateLoop];
return self; 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 { - (void) updateBounds {
float x = FLT_MAX; float x = FLT_MAX;
float y = FLT_MAX; float y = FLT_MAX;
@@ -197,16 +190,17 @@ static const int LABEL_DY = 20;
[self updateBounds]; [self updateBounds];
} }
- (void) updateLoop { - (void) startUpdateLoop {
// Stop immediately if the view has been detached [self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
if (self.superview == nil) { }
return;
}
- (void) updateLoop {
[self updateContentsForHost:_host]; [self updateContentsForHost:_host];
// Queue the next refresh cycle // Stop updating when we detach from our parent view
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE]; if (self.superview != nil) {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
} }
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture { - (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 { - (void) hostClicked {
[_callback hostClicked:_host view:self]; [_callback hostClicked:_host view:self];
} }
-8
View File
@@ -27,14 +27,6 @@ FOUNDATION_EXPORT NSString *const deviceName;
+ (NSData*) hexToBytes:(NSString*) hex; + (NSData*) hexToBytes:(NSString*) hex;
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog; + (void) addHelpOptionToDialog:(UIAlertController*)dialog;
+ (BOOL) isActiveNetworkVPN; + (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 @end
+1 -74
View File
@@ -64,88 +64,15 @@ NSString *const deviceName = @"roth";
return NO; return NO;
} }
#if !TARGET_OS_TV
+ (void) launchUrl:(NSString*)urlString {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil];
}
#endif
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog { + (void) addHelpOptionToDialog:(UIAlertController*)dialog {
#if !TARGET_OS_TV #if !TARGET_OS_TV
// tvOS doesn't have a browser // tvOS doesn't have a browser
[dialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ [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 #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 @end
@implementation NSString (NSStringWithTrim) @implementation NSString (NSStringWithTrim)
@@ -40,7 +40,6 @@
@implementation MainFrameViewController { @implementation MainFrameViewController {
NSOperationQueue* _opQueue; NSOperationQueue* _opQueue;
TemporaryHost* _selectedHost; TemporaryHost* _selectedHost;
BOOL _showHiddenApps;
NSString* _uniqueId; NSString* _uniqueId;
NSData* _clientCert; NSData* _clientCert;
DiscoveryManager* _discMan; DiscoveryManager* _discMan;
@@ -64,7 +63,7 @@ static NSMutableSet* hostList;
// failure callback could be invoked. // failure callback could be invoked.
dispatch_sync(dispatch_get_main_queue(), ^{ dispatch_sync(dispatch_get_main_queue(), ^{
self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing" 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]; preferredStyle:UIAlertControllerStyleAlert];
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { [self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
self->_pairAlert = nil; self->_pairAlert = nil;
@@ -168,7 +167,7 @@ static NSMutableSet* hostList;
// Exempt this host from discovery while handling the applist query // Exempt this host from discovery while handling the applist query
[self->_discMan pauseDiscoveryForHost:host]; [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]; [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 { - (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host {
DataManager* database = [[DataManager alloc] init]; DataManager* database = [[DataManager alloc] init];
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList]; NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
@@ -237,7 +218,6 @@ static NSMutableSet* hostList;
if ([app.id isEqualToString:savedApp.id]) { if ([app.id isEqualToString:savedApp.id]) {
savedApp.name = app.name; savedApp.name = app.name;
savedApp.hdrSupported = app.hdrSupported; savedApp.hdrSupported = app.hdrSupported;
// Don't propagate hidden, because we want the local data to prevail
appAlreadyInList = YES; appAlreadyInList = YES;
break; break;
} }
@@ -295,7 +275,6 @@ static NSMutableSet* hostList;
#endif #endif
[_appManager stopRetrieving]; [_appManager stopRetrieving];
_showHiddenApps = NO;
_selectedHost = nil; _selectedHost = nil;
_sortedAppList = nil; _sortedAppList = nil;
@@ -373,7 +352,7 @@ static NSMutableSet* hostList;
return; 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]; ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
// Exempt this host from discovery while handling the serverinfo request // 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]; [[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) { [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Test Network" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) {
[self showLoadingFrame:^{ [self showLoadingFrame:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 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."; 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 { else {
char blockedPorts[512]; 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";
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]; 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]; UIAlertController* netTestAlert = [UIAlertController alertControllerWithTitle:@"Network Test Complete" message:message preferredStyle:UIAlertControllerStyleAlert];
@@ -532,11 +504,8 @@ static NSMutableSet* hostList;
}]]; }]];
#if !TARGET_OS_TV #if !TARGET_OS_TV
if (host.state != StateOnline) { 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){ [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 #endif
@@ -556,7 +525,9 @@ static NSMutableSet* hostList;
longClickAlert.popoverPresentationController.sourceView = view; 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 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 { - (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]; 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:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ [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:^{ [self showLoadingFrame:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){ [self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
@@ -604,7 +575,6 @@ static NSMutableSet* hostList;
- (void) prepareToStreamApp:(TemporaryApp *)app { - (void) prepareToStreamApp:(TemporaryApp *)app {
_streamConfig = [[StreamConfiguration alloc] init]; _streamConfig = [[StreamConfiguration alloc] init];
_streamConfig.host = app.host.activeAddress; _streamConfig.host = app.host.activeAddress;
_streamConfig.httpsPort = app.host.httpsPort;
_streamConfig.appID = app.id; _streamConfig.appID = app.id;
_streamConfig.appName = app.name; _streamConfig.appName = app.name;
_streamConfig.serverCert = app.host.serverCert; _streamConfig.serverCert = app.host.serverCert;
@@ -635,75 +605,46 @@ static NSMutableSet* hostList;
#endif #endif
_streamConfig.bitRate = [streamSettings.bitrate intValue]; _streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.streamingRemotely = streamSettings.streamingRemotely;
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames; _streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC; _streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
_streamConfig.useFramePacing = streamSettings.useFramePacing; _streamConfig.allowHevc = streamSettings.useHevc;
_streamConfig.swapABXYButtons = streamSettings.swapABXYButtons;
// multiController must be set before calling getConnectedGamepadMask // multiController must be set before calling getConnectedGamepadMask
_streamConfig.multiController = streamSettings.multiController; _streamConfig.multiController = streamSettings.multiController;
_streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig]; _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); // Probe for supported channel configurations
Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels); long outputChannels = [AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
if (numberOfChannels >= 8) { Log(LOG_I, @"Audio device supports %d channels", outputChannels);
if (outputChannels >= 8) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND; _streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
} }
else if (numberOfChannels >= 6) { else if (outputChannels >= 6) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND; _streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
} }
else { else {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO; _streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
} }
_streamConfig.serverCodecModeSupport = app.host.serverCodecModeSupport; // 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+).
switch (streamSettings.preferredCodec) { //
case CODEC_PREF_AV1: // It should also be a user preference, since some games may require higher peak
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0) // brightness than the iOS device can support to look correct in HDR mode.
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) { if (@available(iOS 11.3, *)) {
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN8; _streamConfig.enableHdr =
} app.hdrSupported && // App supported
#endif (app.host.serverCodecModeSupport & 0x200) != 0 && // HEVC Main10 encoding on host PC GPU
// Fall-through VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) && // Decoder supported
(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0 && // Display supported
case CODEC_PREF_AUTO: streamSettings.enableHdr; // User wants it enabled
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;
} }
// 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 { - (void) appClicked:(TemporaryApp *)app {
Log(LOG_D, @"Long clicked app: %@", app.name); Log(LOG_D, @"Clicked app: %@", app.name);
[_appManager stopRetrieving]; [_appManager stopRetrieving];
@@ -717,47 +658,22 @@ static NSMutableSet* hostList;
#endif #endif
TemporaryApp* currentApp = [self findRunningApp:app.host]; 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) { 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: [alertController addAction:[UIAlertAction actionWithTitle:
[app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){ [app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
Log(LOG_I, @"Quitting application: %@", currentApp.name); Log(LOG_I, @"Quitting application: %@", currentApp.name);
[self showLoadingFrame: ^{ [self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 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]; HttpResponse* quitResponse = [[HttpResponse alloc] init];
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]]; HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
@@ -798,6 +714,9 @@ static NSMutableSet* hostList;
else { else {
app.host.currentGame = @"0"; app.host.currentGame = @"0";
dispatch_async(dispatch_get_main_queue(), ^{ 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 it succeeds and we're to start streaming, segue to the stream
if (![app.id isEqualToString:currentApp.id]) { if (![app.id isEqualToString:currentApp.id]) {
[self prepareToStreamApp:app]; [self prepareToStreamApp:app];
@@ -815,46 +734,8 @@ static NSMutableSet* hostList;
}]; }];
}]]; }]];
} [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
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];
} else { } else {
[self prepareToStreamApp:app]; [self prepareToStreamApp:app];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil]; [self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
@@ -883,7 +764,7 @@ static NSMutableSet* hostList;
#if TARGET_OS_TV #if TARGET_OS_TV
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self appClicked:_sortedAppList[indexPath.row] view:nil]; [self appClicked:_sortedAppList[indexPath.row]];
} }
#endif #endif
@@ -987,11 +868,6 @@ static NSMutableSet* hostList;
self.collectionView.allowsMultipleSelection = NO; self.collectionView.allowsMultipleSelection = NO;
#if !TARGET_OS_TV #if !TARGET_OS_TV
self.collectionView.multipleTouchEnabled = NO; 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 #endif
[self retrieveSavedHosts]; [self retrieveSavedHosts];
@@ -1007,21 +883,6 @@ static NSMutableSet* hostList;
} }
#if TARGET_OS_TV #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 - (void)openTvSettings:(id)sender
{ {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
@@ -1180,17 +1041,14 @@ static NSMutableSet* hostList;
} }
- (void) updateAllHosts:(NSArray *)hosts { - (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(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
Log(LOG_D, @"New host list:"); 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); 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) { @synchronized(hostList) {
[hostList removeAllObjects]; [hostList removeAllObjects];
[hostList addObjectsFromArray:hostsCopy]; [hostList addObjectsFromArray:hosts];
} }
[self updateHosts]; [self updateHosts];
}); });
@@ -1198,25 +1056,27 @@ static NSMutableSet* hostList;
- (void)updateHostShortcuts { - (void)updateHostShortcuts {
#if !TARGET_OS_TV #if !TARGET_OS_TV
NSMutableArray* quickActions = [[NSMutableArray alloc] init]; if (@available(iOS 9.0, *)) {
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
@synchronized (hostList) { @synchronized (hostList) {
for (TemporaryHost* host in hostList) { for (TemporaryHost* host in hostList) {
// Pair state may be unknown if we haven't polled it yet, but the app list // Pair state may be unknown if we haven't polled it yet, but the app list
// count will persist from paired PCs // count will persist from paired PCs
if ([host.appList count] > 0) { if ([host.appList count] > 0) {
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc] UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
initWithType:@"PC" initWithType:@"PC"
localizedTitle:host.name localizedTitle:host.name
localizedSubtitle:nil localizedSubtitle:nil
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay] icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]]; userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
[quickActions addObject: shortcut]; [quickActions addObject: shortcut];
}
} }
} }
}
[UIApplication sharedApplication].shortcutItems = quickActions; [UIApplication sharedApplication].shortcutItems = quickActions;
}
#endif #endif
} }
@@ -1329,16 +1189,6 @@ static NSMutableSet* hostList;
_sortedAppList = [host.appList allObjects]; _sortedAppList = [host.appList allObjects];
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)]; _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]; [hostScrollView removeFromSuperview];
[self.collectionView reloadData]; [self.collectionView reloadData];
} }
@@ -1358,12 +1208,12 @@ static NSMutableSet* hostList;
[cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added [cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added
[cell addSubview:appView]; [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]; UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.bounds];
cell.layer.masksToBounds = NO; cell.layer.masksToBounds = NO;
cell.layer.shadowColor = [UIColor blackColor].CGColor; cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f); cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f);
cell.layer.shadowOpacity = 0.5f;
cell.layer.shadowPath = shadowPath.CGPath; cell.layer.shadowPath = shadowPath.CGPath;
#if !TARGET_OS_TV #if !TARGET_OS_TV
@@ -626,10 +626,10 @@ const int FrontViewPositionNone = 0xff;
_frontViewPosition = FrontViewPositionLeft; _frontViewPosition = FrontViewPositionLeft;
_rearViewPosition = FrontViewPositionLeft; _rearViewPosition = FrontViewPositionLeft;
_rightViewPosition = FrontViewPositionLeft; _rightViewPosition = FrontViewPositionLeft;
_rearViewRevealWidth = 510.0f; _rearViewRevealWidth = 260.0f;
_rearViewRevealOverdraw = 60.0f; _rearViewRevealOverdraw = 60.0f;
_rearViewRevealDisplacement = 40.0f; _rearViewRevealDisplacement = 40.0f;
_rightViewRevealWidth = 510.0f; _rightViewRevealWidth = 260.0f;
_rightViewRevealOverdraw = 60.0f; _rightViewRevealOverdraw = 60.0f;
_rightViewRevealDisplacement = 40.0f; _rightViewRevealDisplacement = 40.0f;
_bounceBackOnOverdraw = YES; _bounceBackOnOverdraw = YES;
@@ -1387,11 +1387,15 @@ const int FrontViewPositionNone = 0xff;
// These are derived from the primary view controller // These are derived from the primary view controller
if (@available(iOS 11.0, *)) { if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden]; // iOS 11 betas (which are still installed in some places :\) crash with
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; // doesNotRecognizeSelector since this wasn't added until after the iPhone X
} // announcement.
if (@available(iOS 14.0, *)) { if ([self respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) {
[self setNeedsUpdateOfPrefersPointerLocked]; [self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
if ([self respondsToSelector: @selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]) {
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
} }
} }
@@ -1405,11 +1409,6 @@ const int FrontViewPositionNone = 0xff;
return _primaryViewController; return _primaryViewController;
} }
- (UIViewController*)childViewControllerForPointerLock
{
return _primaryViewController;
}
#pragma mark Animated view controller deployment and layout #pragma mark Animated view controller deployment and layout
// Primitive method for view controller deployment and animated layout to the given position. // Primitive method for view controller deployment and animated layout to the given position.

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