diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index fb0e827..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -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.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
deleted file mode 100644
index 3ba13e0..0000000
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ /dev/null
@@ -1 +0,0 @@
-blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 066b2d9..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -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.
diff --git a/.github/auto-comment.yml b/.github/auto-comment.yml
deleted file mode 100644
index e6985d2..0000000
--- a/.github/auto-comment.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-issuesOpened: >
- If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.
- This issue tracker should only be used for specific bugs or feature requests.
- Thank you, and happy streaming!
diff --git a/.github/lock.yml b/.github/lock.yml
deleted file mode 100644
index 1ba1162..0000000
--- a/.github/lock.yml
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/.github/no-response.yml b/.github/no-response.yml
deleted file mode 100644
index 156bc9e..0000000
--- a/.github/no-response.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-# ProBot No Response (https://probot.github.io/apps/no-response/)
-
-daysUntilClose: 7
-responseRequiredLabel: 'need more info'
-closeComment: >
- This issue has been automatically closed because there was no response to a
- request for more information from the issue opener. Please leave a comment or
- open a new issue if you have additional information related to this issue.
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index df9115c..0000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-# ProBot Stale (https://probot.github.io/apps/stale/)
-
-daysUntilStale: 90
-daysUntilClose: 7
-exemptLabels:
- - accepted
- - bug
- - enhancement
- - meta
-staleLabel: stale
-markComment: >
- This issue has been automatically marked as stale because it has not had
- recent activity. It will be closed if no further activity occurs.
-closeComment: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 4c75fde..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -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
diff --git a/.gitignore b/.gitignore
index 861c417..3c53649 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,4 @@
**/xcuserdata/
**/Moonlight.xcscmblueprint
-**/xcschemes/*.xcscheme
-.DS_Store
Build
DerivedData
diff --git a/.gitmodules b/.gitmodules
index ca517c2..11cd918 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
[submodule "moonlight-common/moonlight-common-c"]
path = moonlight-common/moonlight-common-c
url = https://github.com/moonlight-stream/moonlight-common-c.git
-[submodule "X1Kit"]
- path = X1Kit
- url = https://github.com/cgutman/X1Kit.git
diff --git a/BuildScripts/Opus/build.sh b/BuildScripts/Opus/build.sh
new file mode 100755
index 0000000..283dba8
--- /dev/null
+++ b/BuildScripts/Opus/build.sh
@@ -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
diff --git a/BuildScripts/Opus/combine.sh b/BuildScripts/Opus/combine.sh
new file mode 100755
index 0000000..33c0c13
--- /dev/null
+++ b/BuildScripts/Opus/combine.sh
@@ -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
diff --git a/BuildScripts/Opus/config.sh b/BuildScripts/Opus/config.sh
new file mode 100755
index 0000000..63a9f44
--- /dev/null
+++ b/BuildScripts/Opus/config.sh
@@ -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
+
+
+
diff --git a/BuildScripts/Opus/sync.sh b/BuildScripts/Opus/sync.sh
new file mode 100755
index 0000000..31f5239
--- /dev/null
+++ b/BuildScripts/Opus/sync.sh
@@ -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
diff --git a/BuildScripts/build-ffmpeg.sh b/BuildScripts/build-ffmpeg.sh
new file mode 100755
index 0000000..7edfab9
--- /dev/null
+++ b/BuildScripts/build-ffmpeg.sh
@@ -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"
+
diff --git a/BuildScripts/build-libopus.sh b/BuildScripts/build-libopus.sh
deleted file mode 100755
index aa4e568..0000000
--- a/BuildScripts/build-libopus.sh
+++ /dev/null
@@ -1,180 +0,0 @@
-#!/bin/bash
-# Builds libopus for iOS or tvOS.
-#
-# Copyright 2012 Mike Tigas
-#
-# 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."
diff --git a/BuildScripts/build-openssl.sh b/BuildScripts/build-openssl.sh
new file mode 100755
index 0000000..8aafd96
--- /dev/null
+++ b/BuildScripts/build-openssl.sh
@@ -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"
+
diff --git a/Builds/Limelight_0_1.ipa b/Builds/Limelight_0_1.ipa
new file mode 100644
index 0000000..cb8e9b4
Binary files /dev/null and b/Builds/Limelight_0_1.ipa differ
diff --git a/Builds/Limelight_0_2.ipa b/Builds/Limelight_0_2.ipa
new file mode 100644
index 0000000..bf6a715
Binary files /dev/null and b/Builds/Limelight_0_2.ipa differ
diff --git a/Builds/Limelight_0_2_1.ipa b/Builds/Limelight_0_2_1.ipa
new file mode 100644
index 0000000..163a651
Binary files /dev/null and b/Builds/Limelight_0_2_1.ipa differ
diff --git a/Builds/Limelight_0_2_2.ipa b/Builds/Limelight_0_2_2.ipa
new file mode 100644
index 0000000..928578f
Binary files /dev/null and b/Builds/Limelight_0_2_2.ipa differ
diff --git a/Builds/Limelight_0_3_0.ipa b/Builds/Limelight_0_3_0.ipa
new file mode 100644
index 0000000..74c1a68
Binary files /dev/null and b/Builds/Limelight_0_3_0.ipa differ
diff --git a/Builds/Limelight_0_3_1.ipa b/Builds/Limelight_0_3_1.ipa
new file mode 100644
index 0000000..b8b3298
Binary files /dev/null and b/Builds/Limelight_0_3_1.ipa differ
diff --git a/Builds/Limelight_0_3_2.ipa b/Builds/Limelight_0_3_2.ipa
new file mode 100644
index 0000000..fdec6d5
Binary files /dev/null and b/Builds/Limelight_0_3_2.ipa differ
diff --git a/Builds/Limelight_0_4_0.ipa b/Builds/Limelight_0_4_0.ipa
new file mode 100644
index 0000000..a053dcf
Binary files /dev/null and b/Builds/Limelight_0_4_0.ipa differ
diff --git a/Builds/Limelight_0_4_1.ipa b/Builds/Limelight_0_4_1.ipa
new file mode 100644
index 0000000..43bbfcb
Binary files /dev/null and b/Builds/Limelight_0_4_1.ipa differ
diff --git a/Builds/Limelight_0_9_0.ipa b/Builds/Limelight_0_9_0.ipa
new file mode 100644
index 0000000..ee5dbee
Binary files /dev/null and b/Builds/Limelight_0_9_0.ipa differ
diff --git a/Builds/Moonlight_0_9_2.ipa b/Builds/Moonlight_0_9_2.ipa
new file mode 100644
index 0000000..8662f9a
Binary files /dev/null and b/Builds/Moonlight_0_9_2.ipa differ
diff --git a/LICENSE.txt b/LICENSE.txt
index 10c41a5..94a9ed0 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -672,11 +672,3 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
-
-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.
diff --git a/Launch Screen.xib b/Launch Screen.xib
index c238181..bfeb7af 100644
--- a/Launch Screen.xib
+++ b/Launch Screen.xib
@@ -1,19 +1,51 @@
-
-
-
+
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Limelight/AppDelegate.h b/Limelight/AppDelegate.h
index 90deb40..0985139 100644
--- a/Limelight/AppDelegate.h
+++ b/Limelight/AppDelegate.h
@@ -11,8 +11,6 @@
@interface AppDelegate : UIResponder
@property (strong, nonatomic) UIWindow *window;
-@property (strong, nonatomic) NSString *pcUuidToLoad;
-@property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL);
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
diff --git a/Limelight/AppDelegate.m b/Limelight/AppDelegate.m
index 5db73f5..2a3b8ec 100644
--- a/Limelight/AppDelegate.m
+++ b/Limelight/AppDelegate.m
@@ -16,29 +16,39 @@
static NSOperationQueue* mainQueue;
-#if TARGET_OS_TV
-static NSString* DB_NAME = @"Moonlight_tvOS.bin";
-#else
-static NSString* DB_NAME = @"Limelight_iOS.sqlite";
-#endif
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ [[UILabel appearance] setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
+ [[UIButton appearance].titleLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
-- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
-#if !TARGET_OS_TV
- UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
- if (shortcut != nil) {
- _pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
- }
-#endif
+
+ // Generate selected segment background image
+ CGSize borderImageSize = CGSizeMake(1.f, 100.f);
+
+ UIGraphicsBeginImageContext(borderImageSize);
+ CGContextRef context = UIGraphicsGetCurrentContext();
+
+ CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
+ CGContextFillRect(context, CGRectMake(0.f, borderImageSize.height * 0.8, borderImageSize.width, borderImageSize.height));
+
+ UIImage *selectedSegmentBG = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+ // Clear default border and background color
+ [[UISegmentedControl appearance] setBackgroundImage:[[UIImage alloc] init] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
+ [[UISegmentedControl appearance] setDividerImage:[[UIImage alloc] init] forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
+
+ // Set selected segment background image
+ [[UISegmentedControl appearance] setBackgroundImage:[selectedSegmentBG imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
+
+ // Change font on UISegmentedControl
+ [[UISegmentedControl appearance] setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
+ [UIColor whiteColor], NSForegroundColorAttributeName,
+ [UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]], NSFontAttributeName, nil] forState:UIControlStateNormal];
+
return YES;
}
-#if !TARGET_OS_TV
-- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler {
- _pcUuidToLoad = (NSString*)[shortcutItem.userInfo objectForKey:@"UUID"];
- _shortcutCompletionHandler = completionHandler;
-}
-#endif
-
- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
@@ -47,7 +57,7 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
- (void)applicationDidEnterBackground:(UIApplication *)application
{
- // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
@@ -69,22 +79,12 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
- (void)saveContext
{
- NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
+ NSError *error = nil;
+ NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
- [managedObjectContext performBlock:^{
- if (![managedObjectContext hasChanges]) {
- return;
- }
- NSError *error = nil;
- if (![managedObjectContext save:&error]) {
- Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
- }
-
-#if TARGET_OS_TV
- NSData* dbData = [NSData dataWithContentsOfURL:[[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:DB_NAME]];
- [[NSUserDefaults standardUserDefaults] setObject:dbData forKey:DB_NAME];
-#endif
- }];
+ if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
+ Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
+ }
}
}
@@ -125,30 +125,19 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
return _persistentStoreCoordinator;
}
+ NSURL *storeURL = [self getStoreURL];
+
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
- NSString* storeType;
-
-#if TARGET_OS_TV
- // Use a binary store for tvOS since we will need exclusive access to the file
- // to serialize into NSUserDefaults.
- storeType = NSBinaryStoreType;
-#else
- storeType = NSSQLiteStoreType;
-#endif
-
- // We must ensure the persistent store is ready to opened
- [self preparePersistentStore];
-
- if (![_persistentStoreCoordinator addPersistentStoreWithType:storeType configuration:nil URL:[self getStoreURL] options:options error:&error]) {
+ if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
// Log the error
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
// Drop the database
- [self dropDatabase];
+ [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
// Try again
return [self persistentStoreCoordinator];
@@ -165,50 +154,8 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
-- (void) dropDatabase
-{
- // Delete the file on disk
- [[NSFileManager defaultManager] removeItemAtURL:[self getStoreURL] error:nil];
-
-#if TARGET_OS_TV
- // Also delete the copy in the NSUserDefaults on tvOS
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:DB_NAME];
-#endif
-}
-
-- (void) preparePersistentStore
-{
-#if TARGET_OS_TV
- // On tvOS, we may need to inflate the DB from NSUserDefaults
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
- NSString *cacheDirectory = [paths objectAtIndex:0];
- NSString *dbPath = [cacheDirectory stringByAppendingPathComponent:DB_NAME];
-
- // Always prefer the on disk version
- if (![[NSFileManager defaultManager] fileExistsAtPath:dbPath]) {
- // If that is unavailable, inflate it from NSUserDefaults
- NSData* data = [[NSUserDefaults standardUserDefaults] dataForKey:DB_NAME];
- if (data != nil) {
- Log(LOG_I, @"Inflating database from NSUserDefaults");
- [data writeToFile:dbPath atomically:YES];
- }
- else {
- Log(LOG_I, @"No database on disk or in NSUserDefaults");
- }
- }
- else {
- Log(LOG_I, @"Using cached database");
- }
-#endif
-}
-
- (NSURL*) getStoreURL {
-#if TARGET_OS_TV
- // We use the cache folder to store our database on tvOS
- return [[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:DB_NAME];
-#else
- return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:DB_NAME];
-#endif
+ return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Limelight_iOS.sqlite"];
}
@end
diff --git a/Limelight/Crypto/CryptoManager.h b/Limelight/Crypto/CryptoManager.h
index b1fc76d..f716083 100644
--- a/Limelight/Crypto/CryptoManager.h
+++ b/Limelight/Crypto/CryptoManager.h
@@ -6,14 +6,16 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
+
@interface CryptoManager : NSObject
-+ (void) generateKeyPairUsingSSL;
++ (void) generateKeyPairUsingSSl;
+ (NSData*) readCertFromFile;
+ (NSData*) readKeyFromFile;
+ (NSData*) readP12FromFile;
+ (NSData*) getSignatureFromCert:(NSData*)cert;
-+ (NSData*) pemToDer:(NSData*)pemCertBytes;
++ (NSData*) nullTerminateString:(NSData*)data;
- (NSData*) createAESKeyFromSaltSHA1:(NSData*)saltedPIN;
- (NSData*) createAESKeyFromSaltSHA256:(NSData*)saltedPIN;
diff --git a/Limelight/Crypto/CryptoManager.m b/Limelight/Crypto/CryptoManager.m
index f9603cc..38b4e8b 100644
--- a/Limelight/Crypto/CryptoManager.m
+++ b/Limelight/Crypto/CryptoManager.m
@@ -9,6 +9,7 @@
#import "CryptoManager.h"
#import "mkcert.h"
+#include
#include
#include
#include
@@ -44,72 +45,59 @@ static NSData* p12 = nil;
}
- (NSData*) aesEncrypt:(NSData*)data withKey:(NSData*)key {
- EVP_CIPHER_CTX* cipher;
- int ciphertextLen;
-
- cipher = EVP_CIPHER_CTX_new();
-
- EVP_EncryptInit(cipher, EVP_aes_128_ecb(), [key bytes], NULL);
- EVP_CIPHER_CTX_set_padding(cipher, 0);
-
- NSMutableData* ciphertext = [NSMutableData dataWithLength:[data length]];
- EVP_EncryptUpdate(cipher,
- [ciphertext mutableBytes],
- &ciphertextLen,
- [data bytes],
- (int)[data length]);
- assert(ciphertextLen == [ciphertext length]);
-
- EVP_CIPHER_CTX_free(cipher);
+ AES_KEY aesKey;
+ AES_set_encrypt_key([key bytes], 128, &aesKey);
+ int size = [self getEncryptSize:data];
+ unsigned char* buffer = malloc(size);
+ unsigned char* blockRoundedBuffer = calloc(1, size);
+ memcpy(blockRoundedBuffer, [data bytes], [data length]);
- return ciphertext;
+ // AES_encrypt only encrypts the first 16 bytes so iterate the entire buffer
+ int blockOffset = 0;
+ while (blockOffset < size) {
+ AES_encrypt(blockRoundedBuffer + blockOffset, buffer + blockOffset, &aesKey);
+ blockOffset += 16;
+ }
+
+ NSData* encryptedData = [NSData dataWithBytes:buffer length:size];
+ free(buffer);
+ free(blockRoundedBuffer);
+ return encryptedData;
}
- (NSData*) aesDecrypt:(NSData*)data withKey:(NSData*)key {
- EVP_CIPHER_CTX* cipher;
- int plaintextLen;
-
- cipher = EVP_CIPHER_CTX_new();
-
- EVP_DecryptInit(cipher, EVP_aes_128_ecb(), [key bytes], NULL);
- EVP_CIPHER_CTX_set_padding(cipher, 0);
-
- NSMutableData* plaintext = [NSMutableData dataWithLength:[data length]];
- EVP_DecryptUpdate(cipher,
- [plaintext mutableBytes],
- &plaintextLen,
- [data bytes],
- (int)[data length]);
- assert(plaintextLen == [plaintext length]);
-
- EVP_CIPHER_CTX_free(cipher);
+ AES_KEY aesKey;
+ AES_set_decrypt_key([key bytes], 128, &aesKey);
+ unsigned char* buffer = malloc([data length]);
- return plaintext;
+ // AES_decrypt only decrypts the first 16 bytes so iterate the entire buffer
+ int blockOffset = 0;
+ while (blockOffset < [data length]) {
+ AES_decrypt([data bytes] + blockOffset, buffer + blockOffset, &aesKey);
+ blockOffset += 16;
+ }
+
+ NSData* decryptedData = [NSData dataWithBytes:buffer length:[data length]];
+ free(buffer);
+ return decryptedData;
}
-+ (NSData*) pemToDer:(NSData*)pemCertBytes {
- X509* x509;
-
- BIO* bio = BIO_new_mem_buf([pemCertBytes bytes], (int)[pemCertBytes length]);
- x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
- BIO_free(bio);
-
- bio = BIO_new(BIO_s_mem());
- i2d_X509_bio(bio, x509);
- X509_free(x509);
+- (int) getEncryptSize:(NSData*)data {
+ // the size is the length of the data ceiling to the nearest 16 bytes
+ return (((int)[data length] + 15) / 16) * 16;
+}
- BUF_MEM* mem;
- BIO_get_mem_ptr(bio, &mem);
-
- NSData* ret = [[NSData alloc] initWithBytes:mem->data length:mem->length];
- BIO_free(bio);
-
- return ret;
++ (NSData*) nullTerminateString:(NSData*)data {
+ NSMutableData* mutData = [NSMutableData dataWithData:data];
+ [mutData appendBytes:"" length:1];
+ return mutData;
}
- (bool) verifySignature:(NSData *)data withSignature:(NSData*)signature andCert:(NSData*)cert {
+ const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
X509* x509;
- BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
+ BIO* bio = BIO_new(BIO_s_mem());
+ BIO_puts(bio, buffer);
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free(bio);
@@ -134,7 +122,9 @@ static NSData* p12 = nil;
}
- (NSData *)signData:(NSData *)data withKey:(NSData *)key {
- BIO* bio = BIO_new_mem_buf([key bytes], (int)[key length]);
+ const char* buffer = [[CryptoManager nullTerminateString:key] bytes];
+ BIO* bio = BIO_new(BIO_s_mem());
+ BIO_puts(bio, buffer);
EVP_PKEY* pkey;
pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
@@ -169,137 +159,82 @@ static NSData* p12 = nil;
return signedData;
}
-+ (NSData*) readCryptoObject:(NSString*)item {
-#if TARGET_OS_TV
- return [[NSUserDefaults standardUserDefaults] dataForKey:item];
-#else
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
- NSString *documentsDirectory = [paths objectAtIndex:0];
- NSString *file = [documentsDirectory stringByAppendingPathComponent:item];
- return [NSData dataWithContentsOfFile:file];
-#endif
-}
-
-+ (void) writeCryptoObject:(NSString*)item data:(NSData*)data {
-#if TARGET_OS_TV
- [[NSUserDefaults standardUserDefaults] setObject:data forKey:item];
-#else
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
- NSString *documentsDirectory = [paths objectAtIndex:0];
- NSString *file = [documentsDirectory stringByAppendingPathComponent:item];
- [data writeToFile:file atomically:NO];
-#endif
-}
-
+// TODO: these three methods are almost identical, fix the copy-pasta
+ (NSData*) readCertFromFile {
if (cert == nil) {
- cert = [CryptoManager readCryptoObject:@"client.crt"];
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDirectory = [paths objectAtIndex:0];
+ NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
+ cert = [NSData dataWithContentsOfFile:certFile];
}
return cert;
}
+ (NSData*) readP12FromFile {
if (p12 == nil) {
- p12 = [CryptoManager readCryptoObject:@"client.p12"];
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDirectory = [paths objectAtIndex:0];
+ NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
+ p12 = [NSData dataWithContentsOfFile:p12File];
}
return p12;
}
+ (NSData*) readKeyFromFile {
if (key == nil) {
- key = [CryptoManager readCryptoObject:@"client.key"];
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDirectory = [paths objectAtIndex:0];
+ NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
+ key = [NSData dataWithContentsOfFile:keyFile];
}
return key;
}
+ (bool) keyPairExists {
- bool keyFileExists = [CryptoManager readCryptoObject:@"client.key"] != nil;
- bool p12FileExists = [CryptoManager readCryptoObject:@"client.p12"] != nil;
- bool certFileExists = [CryptoManager readCryptoObject:@"client.crt"] != nil;
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDirectory = [paths objectAtIndex:0];
+ NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
+ NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
+ NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
+
+ bool keyFileExists = [[NSFileManager defaultManager] fileExistsAtPath:keyFile];
+ bool p12FileExists = [[NSFileManager defaultManager] fileExistsAtPath:p12File];
+ bool certFileExists = [[NSFileManager defaultManager] fileExistsAtPath:certFile];
return keyFileExists && p12FileExists && certFileExists;
}
+ (NSData *)getSignatureFromCert:(NSData *)cert {
- BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
- X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
- BIO_free(bio);
+ const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
+ X509* x509;
+ BIO* bio = BIO_new(BIO_s_mem());
+ BIO_puts(bio, buffer);
+ x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
if (!x509) {
Log(LOG_E, @"Unable to parse certificate in memory!");
return NULL;
}
-
-#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
- ASN1_BIT_STRING *asnSignature = x509->signature;
-#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
- ASN1_BIT_STRING *asnSignature;
- X509_get0_signature(&asnSignature, NULL, x509);
-#else
- const ASN1_BIT_STRING *asnSignature;
- X509_get0_signature(&asnSignature, NULL, x509);
-#endif
-
- NSData* sig = [NSData dataWithBytes:asnSignature->data length:asnSignature->length];
-
- X509_free(x509);
-
- return sig;
+ return [NSData dataWithBytes:x509->signature->data length:x509->signature->length];
}
-+ (NSData*)getKeyFromCertKeyPair:(CertKeyPair*)certKeyPair {
- BIO* bio = BIO_new(BIO_s_mem());
-
- PEM_write_bio_PrivateKey_traditional(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
-
- BUF_MEM* mem;
- BIO_get_mem_ptr(bio, &mem);
- NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
- BIO_free(bio);
- return data;
-}
-
-+ (NSData*)getP12FromCertKeyPair:(CertKeyPair*)certKeyPair {
- BIO* bio = BIO_new(BIO_s_mem());
-
- i2d_PKCS12_bio(bio, certKeyPair->p12);
-
- BUF_MEM* mem;
- BIO_get_mem_ptr(bio, &mem);
- NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
- BIO_free(bio);
- return data;
-}
-
-+ (NSData*)getCertFromCertKeyPair:(CertKeyPair*)certKeyPair {
- BIO* bio = BIO_new(BIO_s_mem());
-
- PEM_write_bio_X509(bio, certKeyPair->x509);
-
- BUF_MEM* mem;
- BIO_get_mem_ptr(bio, &mem);
- NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
- BIO_free(bio);
- return data;
-}
-
-+ (void) generateKeyPairUsingSSL {
++ (void) generateKeyPairUsingSSl {
static dispatch_once_t pred;
dispatch_once(&pred, ^{
if (![CryptoManager keyPairExists]) {
+
Log(LOG_I, @"Generating Certificate... ");
CertKeyPair certKeyPair = generateCertKeyPair();
- NSData* certData = [CryptoManager getCertFromCertKeyPair:&certKeyPair];
- NSData* p12Data = [CryptoManager getP12FromCertKeyPair:&certKeyPair];
- NSData* keyData = [CryptoManager getKeyFromCertKeyPair:&certKeyPair];
+ NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString* documentsDirectory = [paths objectAtIndex:0];
+ NSString* certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
+ NSString* keyPairFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
+ NSString* p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
+ //Log(LOG_D, @"Writing cert and key to: \n%@\n%@", certFile, keyPairFile);
+ saveCertKeyPair([certFile UTF8String], [p12File UTF8String], [keyPairFile UTF8String], certKeyPair);
freeCertKeyPair(certKeyPair);
-
- [CryptoManager writeCryptoObject:@"client.crt" data:certData];
- [CryptoManager writeCryptoObject:@"client.p12" data:p12Data];
- [CryptoManager writeCryptoObject:@"client.key" data:keyData];
-
Log(LOG_I, @"Certificate created");
}
});
diff --git a/Limelight/Crypto/IdManager.h b/Limelight/Crypto/IdManager.h
index 1e5aaeb..3fc5fae 100644
--- a/Limelight/Crypto/IdManager.h
+++ b/Limelight/Crypto/IdManager.h
@@ -6,6 +6,8 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
+#import
+
@interface IdManager : NSObject
+ (NSString*) getUniqueId;
diff --git a/Limelight/Crypto/mkcert.c b/Limelight/Crypto/mkcert.c
index b21b812..98a5d49 100644
--- a/Limelight/Crypto/mkcert.c
+++ b/Limelight/Crypto/mkcert.c
@@ -4,88 +4,54 @@
#include
#include
-#include
#include
-#include
-#include
-#include
-#include
+#include
+#include
+
+#ifndef OPENSSL_NO_ENGINE
+#include
+#endif
static const int NUM_BITS = 2048;
static const int SERIAL = 0;
-static const int NUM_YEARS = 20;
+static const int NUM_YEARS = 10;
-void mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
- X509* cert = X509_new();
-
- EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
+int add_ext(X509 *cert, int nid, char *value);
- EVP_PKEY_keygen_init(ctx);
- EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
-
- // pk must be initialized on input
- EVP_PKEY* pk = NULL;
- EVP_PKEY_keygen(ctx, &pk);
-
- EVP_PKEY_CTX_free(ctx);
-
- X509_set_version(cert, 2);
- ASN1_INTEGER_set(X509_get_serialNumber(cert), serial);
-#if OPENSSL_VERSION_NUMBER < 0x10100000L
- X509_gmtime_adj(X509_get_notBefore(cert), 0);
- X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * years);
-#else
- ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
- ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
-
- X509_gmtime_adj(before, 0);
- X509_gmtime_adj(after, 60 * 60 * 24 * 365 * years);
-
- X509_set1_notBefore(cert, before);
- X509_set1_notAfter(cert, after);
-
- ASN1_STRING_free(before);
- ASN1_STRING_free(after);
-#endif
-
- X509_set_pubkey(cert, pk);
-
- X509_NAME* name = X509_get_subject_name(cert);
- X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
- (const unsigned char*)"NVIDIA GameStream Client",
- -1, -1, 0);
- X509_set_issuer_name(cert, name);
-
- X509_sign(cert, pk, EVP_sha256());
-
- *x509p = cert;
- *pkeyp = pk;
-}
-
-struct CertKeyPair generateCertKeyPair(void) {
+struct CertKeyPair generateCertKeyPair() {
+ BIO *bio_err;
X509 *x509 = NULL;
EVP_PKEY *pkey = NULL;
PKCS12 *p12 = NULL;
+
+ CRYPTO_mem_ctrl(CRYPTO_MEM_CHECK_ON);
+ bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
+
+ SSLeay_add_all_algorithms();
+ ERR_load_crypto_strings();
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
-
- char* pass = "limelight";
- p12 = PKCS12_create(pass,
- "GameStream",
- pkey,
- x509,
- NULL,
- NID_pbe_WithSHA1And3_Key_TripleDES_CBC,
- -1, // disable certificate encryption
- 2048,
- -1, // disable the automatic MAC
- 0);
- // MAC it ourselves with SHA1 since iOS refuses to load anything else.
- PKCS12_set_mac(p12, pass, -1, NULL, 0, 1, EVP_sha1());
-
+
+ p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
if (p12 == NULL) {
printf("Error generating a valid PKCS12 certificate.\n");
}
+
+ // Debug Print statements
+ //RSA_print_fp(stdout, pkey->pkey.rsa, 0);
+ //X509_print_fp(stdout, x509);
+ //PEM_write_PUBKEY(stdout, pkey);
+ //PEM_write_PrivateKey(stdout, pkey, NULL, NULL, 0, NULL, NULL);
+ //PEM_write_X509(stdout, x509);
+
+#ifndef OPENSSL_NO_ENGINE
+ ENGINE_cleanup();
+#endif
+ CRYPTO_cleanup_all_ex_data();
+
+ CRYPTO_mem_leaks(bio_err);
+ BIO_free(bio_err);
return (CertKeyPair){x509, pkey, p12};
}
@@ -95,3 +61,107 @@ void freeCertKeyPair(struct CertKeyPair certKeyPair) {
EVP_PKEY_free(certKeyPair.pkey);
PKCS12_free(certKeyPair.p12);
}
+
+void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair) {
+ FILE* certFilePtr = fopen(certFile, "w");
+ FILE* keyPairFilePtr = fopen(keyPairFile, "w");
+ FILE* p12FilePtr = fopen(p12File, "wb");
+
+ //TODO: error check
+ PEM_write_PrivateKey(keyPairFilePtr, certKeyPair.pkey, NULL, NULL, 0, NULL, NULL);
+ PEM_write_X509(certFilePtr, certKeyPair.x509);
+ i2d_PKCS12_fp(p12FilePtr, certKeyPair.p12);
+
+ fclose(p12FilePtr);
+ fclose(certFilePtr);
+ fclose(keyPairFilePtr);
+}
+
+int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
+ X509 *x;
+ EVP_PKEY *pk;
+ RSA *rsa;
+ X509_NAME *name = NULL;
+
+ if (*pkeyp == NULL) {
+ if ((pk=EVP_PKEY_new()) == NULL) {
+ return(0);
+ }
+ } else {
+ pk = *pkeyp;
+ }
+
+ if (*x509p == NULL) {
+ if ((x = X509_new()) == NULL) {
+ goto err;
+ }
+ } else {
+ x = *x509p;
+ }
+
+ rsa = RSA_generate_key(bits, RSA_F4, NULL, NULL);
+ if (!EVP_PKEY_assign_RSA(pk, rsa)) {
+ goto err;
+ }
+
+ X509_set_version(x, 2);
+ ASN1_INTEGER_set(X509_get_serialNumber(x), serial);
+ X509_gmtime_adj(X509_get_notBefore(x), 0);
+ X509_gmtime_adj(X509_get_notAfter(x), (long)60*60*24*365*years);
+ X509_set_pubkey(x, pk);
+
+ name = X509_get_subject_name(x);
+
+ /* This function creates and adds the entry, working out the
+ * correct string type and performing checks on its length.
+ */
+ X509_NAME_add_entry_by_txt(name,"CN", MBSTRING_ASC, (unsigned char*)"NVIDIA GameStream Client", -1, -1, 0);
+
+ /* Its self signed so set the issuer name to be the same as the
+ * subject.
+ */
+ X509_set_issuer_name(x, name);
+
+ /* Add various extensions: standard extensions */
+ add_ext(x, NID_basic_constraints, "critical,CA:TRUE");
+ add_ext(x, NID_key_usage, "critical,keyCertSign,cRLSign");
+
+ add_ext(x, NID_subject_key_identifier, "hash");
+
+ if (!X509_sign(x, pk, EVP_sha1())) {
+ goto err;
+ }
+
+ *x509p = x;
+ *pkeyp = pk;
+
+ return(1);
+err:
+ return(0);
+}
+
+/* Add extension using V3 code: we can set the config file as NULL
+ * because we wont reference any other sections.
+ */
+
+int add_ext(X509 *cert, int nid, char *value)
+{
+ X509_EXTENSION *ex;
+ X509V3_CTX ctx;
+ /* This sets the 'context' of the extensions. */
+ /* No configuration database */
+ X509V3_set_ctx_nodb(&ctx);
+ /* Issuer and subject certs: both the target since it is self signed,
+ * no request and no CRL
+ */
+ X509V3_set_ctx(&ctx, cert, cert, NULL, NULL, 0);
+ ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, value);
+ if (!ex) {
+ return 0;
+ }
+
+ X509_add_ext(cert, ex, -1);
+ X509_EXTENSION_free(ex);
+ return 1;
+}
+
diff --git a/Limelight/Crypto/mkcert.h b/Limelight/Crypto/mkcert.h
index f3c62f8..22e62f5 100644
--- a/Limelight/Crypto/mkcert.h
+++ b/Limelight/Crypto/mkcert.h
@@ -18,7 +18,8 @@ typedef struct CertKeyPair {
PKCS12 *p12;
} CertKeyPair;
-struct CertKeyPair generateCertKeyPair(void);
+struct CertKeyPair generateCertKeyPair();
void freeCertKeyPair(CertKeyPair);
+void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair);
#endif
diff --git a/Limelight/Database/App+CoreDataProperties.h b/Limelight/Database/App+CoreDataProperties.h
new file mode 100644
index 0000000..8df4ef6
--- /dev/null
+++ b/Limelight/Database/App+CoreDataProperties.h
@@ -0,0 +1,25 @@
+//
+// App+CoreDataProperties.h
+// Moonlight
+//
+// Created by Diego Waxemberg on 7/10/15.
+// Copyright © 2015 Limelight Stream. All rights reserved.
+//
+// Delete this file and regenerate it using "Create NSManagedObject Subclass…"
+// to keep your implementation up to date with your model.
+//
+
+#import "App.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface App (CoreDataProperties)
+
+@property (nullable, nonatomic, retain) NSString *id;
+@property (nullable, nonatomic, retain) NSData *image;
+@property (nullable, nonatomic, retain) NSString *name;
+@property (nullable, nonatomic, retain) Host *host;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Limelight/Database/App.h b/Limelight/Database/App.h
new file mode 100644
index 0000000..660dab7
--- /dev/null
+++ b/Limelight/Database/App.h
@@ -0,0 +1,18 @@
+//
+// App.h
+// Moonlight
+//
+// Created by Diego Waxemberg on 7/10/15.
+// Copyright © 2015 Limelight Stream. All rights reserved.
+//
+
+#import
+#import
+
+@class Host;
+
+@interface App : NSManagedObject
+
+@end
+
+#import "App+CoreDataProperties.h"
diff --git a/Limelight/Database/App.m b/Limelight/Database/App.m
new file mode 100644
index 0000000..edb6355
--- /dev/null
+++ b/Limelight/Database/App.m
@@ -0,0 +1,13 @@
+//
+// App.m
+// Moonlight
+//
+// Created by Diego Waxemberg on 7/10/15.
+// Copyright © 2015 Limelight Stream. All rights reserved.
+//
+
+#import "App.h"
+
+@implementation App
+
+@end
diff --git a/Limelight/Database/DataManager.h b/Limelight/Database/DataManager.h
index 5717a79..d83b3c0 100644
--- a/Limelight/Database/DataManager.h
+++ b/Limelight/Database/DataManager.h
@@ -6,6 +6,7 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "AppDelegate.h"
#import "TemporaryHost.h"
#import "TemporaryApp.h"
@@ -13,26 +14,12 @@
@interface DataManager : NSObject
-- (void) saveSettingsWithBitrate:(NSInteger)bitrate
- framerate:(NSInteger)framerate
- height:(NSInteger)height
- width:(NSInteger)width
- audioConfig:(NSInteger)audioConfig
- onscreenControls:(NSInteger)onscreenControls
- optimizeGames:(BOOL)optimizeGames
- multiController:(BOOL)multiController
- swapABXYButtons:(BOOL)swapABXYButtons
- audioOnPC:(BOOL)audioOnPC
- preferredCodec:(uint32_t)preferredCodec
- useFramePacing:(BOOL)useFramePacing
- enableHdr:(BOOL)enableHdr
- btMouseSupport:(BOOL)btMouseSupport
- absoluteTouchMode:(BOOL)absoluteTouchMode
- statsOverlay:(BOOL)statsOverlay;
+- (void) saveSettingsWithBitrate:(NSInteger)bitrate framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width onscreenControls:(NSInteger)onscreenControls;
- (NSArray*) getHosts;
- (void) updateHost:(TemporaryHost*)host;
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
+- (void) updateIconForExistingApp:(TemporaryApp*)app;
- (void) removeHost:(TemporaryHost*)host;
- (void) removeApp:(TemporaryApp*)app;
diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m
index 0f5edeb..1f1aee3 100644
--- a/Limelight/Database/DataManager.m
+++ b/Limelight/Database/DataManager.m
@@ -9,6 +9,7 @@
#import "DataManager.h"
#import "TemporaryApp.h"
#import "TemporarySettings.h"
+#import "Settings.h"
@implementation DataManager {
NSManagedObjectContext *_managedObjectContext;
@@ -18,19 +19,9 @@
- (id) init {
self = [super init];
- // HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
- // Main Thread Checker happy.
- if ([NSThread isMainThread]) {
- _appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
- }
- else {
- dispatch_sync(dispatch_get_main_queue(), ^{
- self->_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
- });
- }
-
+ _appDelegate = [[UIApplication sharedApplication] delegate];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
- [_managedObjectContext setParentContext:[_appDelegate managedObjectContext]];
+ [_managedObjectContext setPersistentStoreCoordinator:_appDelegate.persistentStoreCoordinator];
return self;
}
@@ -52,41 +43,16 @@
return uid;
}
-- (void) saveSettingsWithBitrate:(NSInteger)bitrate
- framerate:(NSInteger)framerate
- height:(NSInteger)height
- width:(NSInteger)width
- audioConfig:(NSInteger)audioConfig
- onscreenControls:(NSInteger)onscreenControls
- optimizeGames:(BOOL)optimizeGames
- multiController:(BOOL)multiController
- swapABXYButtons:(BOOL)swapABXYButtons
- audioOnPC:(BOOL)audioOnPC
- preferredCodec:(uint32_t)preferredCodec
- useFramePacing:(BOOL)useFramePacing
- enableHdr:(BOOL)enableHdr
- btMouseSupport:(BOOL)btMouseSupport
- absoluteTouchMode:(BOOL)absoluteTouchMode
- statsOverlay:(BOOL)statsOverlay {
+- (void) saveSettingsWithBitrate:(NSInteger)bitrate framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width onscreenControls:(NSInteger)onscreenControls {
[_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings];
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
+ // Bitrate is persisted in kbps
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
settingsToSave.height = [NSNumber numberWithInteger:height];
settingsToSave.width = [NSNumber numberWithInteger:width];
- settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig];
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
- settingsToSave.optimizeGames = optimizeGames;
- settingsToSave.multiController = multiController;
- settingsToSave.swapABXYButtons = swapABXYButtons;
- settingsToSave.playAudioOnPC = audioOnPC;
- settingsToSave.preferredCodec = preferredCodec;
- settingsToSave.useFramePacing = useFramePacing;
- settingsToSave.enableHdr = enableHdr;
- settingsToSave.btMouseSupport = btMouseSupport;
- settingsToSave.absoluteTouchMode = absoluteTouchMode;
- settingsToSave.statsOverlay = statsOverlay;
[self saveData];
}];
@@ -97,8 +63,8 @@
// Add a new persistent managed object if one doesn't exist
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
if (parent == nil) {
- NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:self->_managedObjectContext];
- parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
+ NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:_managedObjectContext];
+ parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
}
// Push changes from the temp host to the persistent one
@@ -122,8 +88,8 @@
// Add a new persistent managed object if one doesn't exist
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:appRecords];
if (parentApp == nil) {
- NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:self->_managedObjectContext];
- parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
+ NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:_managedObjectContext];
+ parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
}
[app propagateChangesToParent:parentApp withHost:parent];
@@ -137,6 +103,20 @@
}];
}
+- (void) updateIconForExistingApp:(TemporaryApp*)app {
+ [_managedObjectContext performBlockAndWait:^{
+ App* parentApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
+ if (parentApp == nil) {
+ // The app must exist to be updated
+ return;
+ }
+
+ parentApp.image = app.image;
+
+ [self saveData];
+ }];
+}
+
- (TemporarySettings*) getSettings {
__block TemporarySettings *tempSettings;
@@ -165,7 +145,7 @@
[_managedObjectContext performBlockAndWait:^{
App* managedApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
if (managedApp != nil) {
- [self->_managedObjectContext deleteObject:managedApp];
+ [_managedObjectContext deleteObject:managedApp];
[self saveData];
}
}];
@@ -175,7 +155,7 @@
[_managedObjectContext performBlockAndWait:^{
Host* managedHost = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
if (managedHost != nil) {
- [self->_managedObjectContext deleteObject:managedHost];
+ [_managedObjectContext deleteObject:managedHost];
[self saveData];
}
}];
@@ -183,7 +163,7 @@
- (void) saveData {
NSError* error;
- if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
+ if (![_managedObjectContext save:&error]) {
Log(LOG_E, @"Unable to save hosts to database: %@", error);
}
@@ -219,7 +199,7 @@
- (App*) getAppForTemporaryApp:(TemporaryApp*)tempApp withAppRecords:(NSArray*)apps {
for (App* app in apps) {
if ([app.id isEqualToString:tempApp.id] &&
- [app.host.uuid isEqualToString:tempApp.host.uuid]) {
+ [app.host.uuid isEqualToString:app.host.uuid]) {
return app;
}
}
diff --git a/Limelight/Database/Host+CoreDataProperties.h b/Limelight/Database/Host+CoreDataProperties.h
new file mode 100644
index 0000000..8e22d42
--- /dev/null
+++ b/Limelight/Database/Host+CoreDataProperties.h
@@ -0,0 +1,38 @@
+//
+// Host+CoreDataProperties.h
+// Moonlight
+//
+// Created by Diego Waxemberg on 7/10/15.
+// Copyright © 2015 Limelight Stream. All rights reserved.
+//
+// Delete this file and regenerate it using "Create NSManagedObject Subclass…"
+// to keep your implementation up to date with your model.
+//
+
+#import "Host.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface Host (CoreDataProperties)
+
+@property (nullable, nonatomic, retain) NSString *address;
+@property (nullable, nonatomic, retain) NSString *externalAddress;
+@property (nullable, nonatomic, retain) NSString *localAddress;
+@property (nullable, nonatomic, retain) NSString *mac;
+@property (nullable, nonatomic, retain) NSString *name;
+@property (nullable, nonatomic, retain) NSNumber *pairState;
+@property (nullable, nonatomic, retain) NSString *uuid;
+@property (nullable, nonatomic, retain) NSSet *appList;
+
+@end
+
+@interface Host (CoreDataGeneratedAccessors)
+
+- (void)addAppListObject:(NSManagedObject *)value;
+- (void)removeAppListObject:(NSManagedObject *)value;
+- (void)addAppList:(NSSet *)values;
+- (void)removeAppList:(NSSet *)values;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Limelight/Database/Host.h b/Limelight/Database/Host.h
new file mode 100644
index 0000000..d2cb19a
--- /dev/null
+++ b/Limelight/Database/Host.h
@@ -0,0 +1,17 @@
+//
+// Host.h
+// Moonlight
+//
+// Created by Diego Waxemberg on 7/10/15.
+// Copyright © 2015 Limelight Stream. All rights reserved.
+//
+
+#import
+#import
+#import "Utils.h"
+
+@interface Host : NSManagedObject
+
+@end
+
+#import "Host+CoreDataProperties.h"
diff --git a/Limelight/Database/Host.m b/Limelight/Database/Host.m
new file mode 100644
index 0000000..bc71c1c
--- /dev/null
+++ b/Limelight/Database/Host.m
@@ -0,0 +1,13 @@
+//
+// Host.m
+// Moonlight
+//
+// Created by Diego Waxemberg on 7/10/15.
+// Copyright © 2015 Limelight Stream. All rights reserved.
+//
+
+#import "Host.h"
+
+@implementation Host
+
+@end
diff --git a/Limelight/Database/Settings.h b/Limelight/Database/Settings.h
new file mode 100644
index 0000000..39aaa5b
--- /dev/null
+++ b/Limelight/Database/Settings.h
@@ -0,0 +1,22 @@
+//
+// Settings.h
+// Moonlight
+//
+// Created by Diego Waxemberg on 10/28/14.
+// Copyright (c) 2014 Moonlight Stream. All rights reserved.
+//
+
+#import
+#import
+
+
+@interface Settings : NSManagedObject
+
+@property (nonatomic, retain) NSNumber * bitrate;
+@property (nonatomic, retain) NSNumber * framerate;
+@property (nonatomic, retain) NSNumber * height;
+@property (nonatomic, retain) NSNumber * width;
+@property (nonatomic, retain) NSNumber * onscreenControls;
+@property (nonatomic, retain) NSString * uniqueId;
+
+@end
diff --git a/Limelight/Database/Settings.m b/Limelight/Database/Settings.m
new file mode 100644
index 0000000..0ad9ad8
--- /dev/null
+++ b/Limelight/Database/Settings.m
@@ -0,0 +1,21 @@
+//
+// Settings.m
+// Moonlight
+//
+// Created by Diego Waxemberg on 10/28/14.
+// Copyright (c) 2014 Moonlight Stream. All rights reserved.
+//
+
+#import "Settings.h"
+
+
+@implementation Settings
+
+@dynamic bitrate;
+@dynamic framerate;
+@dynamic height;
+@dynamic width;
+@dynamic onscreenControls;
+@dynamic uniqueId;
+
+@end
diff --git a/Limelight/Database/TemporaryApp.h b/Limelight/Database/TemporaryApp.h
index 0a205fc..15a8208 100644
--- a/Limelight/Database/TemporaryApp.h
+++ b/Limelight/Database/TemporaryApp.h
@@ -6,16 +6,15 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "TemporaryHost.h"
-#import "App+CoreDataClass.h"
+#import "App.h"
@interface TemporaryApp : NSObject
@property (nullable, nonatomic, retain) NSString *id;
+@property (nullable, nonatomic, retain) NSData *image;
@property (nullable, nonatomic, retain) NSString *name;
-@property (nullable, nonatomic, retain) NSString *installPath;
-@property (nonatomic) BOOL hdrSupported;
-@property (nonatomic) BOOL hidden;
@property (nullable, nonatomic, retain) TemporaryHost *host;
NS_ASSUME_NONNULL_BEGIN
diff --git a/Limelight/Database/TemporaryApp.m b/Limelight/Database/TemporaryApp.m
index b3de384..97ef8e7 100644
--- a/Limelight/Database/TemporaryApp.m
+++ b/Limelight/Database/TemporaryApp.m
@@ -7,6 +7,7 @@
//
#import "TemporaryApp.h"
+#import "App.h"
@implementation TemporaryApp
@@ -14,9 +15,8 @@
self = [self init];
self.id = app.id;
+ self.image = app.image;
self.name = app.name;
- self.hdrSupported = app.hdrSupported;
- self.hidden = app.hidden;
self.host = tempHost;
return self;
@@ -25,8 +25,6 @@
- (void) propagateChangesToParent:(App*)parent withHost:(Host*)host {
parent.id = self.id;
parent.name = self.name;
- parent.hdrSupported = self.hdrSupported;
- parent.hidden = self.hidden;
parent.host = host;
}
@@ -43,7 +41,7 @@
return YES;
}
- if (![object isKindOfClass:[self class]]) {
+ if (![object isKindOfClass:[App class]]) {
return NO;
}
diff --git a/Limelight/Database/TemporaryHost.h b/Limelight/Database/TemporaryHost.h
index 3cf5110..ffb5cdc 100644
--- a/Limelight/Database/TemporaryHost.h
+++ b/Limelight/Database/TemporaryHost.h
@@ -6,31 +6,26 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "Utils.h"
-#import "Host+CoreDataClass.h"
+#import "Host.h"
@interface TemporaryHost : NSObject
-@property (atomic) State state;
-@property (atomic) PairState pairState;
-@property (atomic, nullable, retain) NSString * activeAddress;
-@property (atomic, nullable, retain) NSString * currentGame;
-@property (atomic) unsigned short httpsPort;
-@property (atomic) BOOL isNvidiaServerSoftware;
-
-@property (atomic, nullable, retain) NSData *serverCert;
-@property (atomic, nullable, retain) NSString *address;
-@property (atomic, nullable, retain) NSString *externalAddress;
-@property (atomic, nullable, retain) NSString *localAddress;
-@property (atomic, nullable, retain) NSString *ipv6Address;
-@property (atomic, nullable, retain) NSString *mac;
-@property (atomic) int serverCodecModeSupport;
+@property (nonatomic) BOOL online;
+@property (nonatomic) PairState pairState;
+@property (nonatomic, nullable) NSString * activeAddress;
+@property (nonatomic, nullable) NSString * currentGame;
NS_ASSUME_NONNULL_BEGIN
-@property (atomic, retain) NSString *name;
-@property (atomic, retain) NSString *uuid;
-@property (atomic, retain) NSSet *appList;
+@property (nonatomic, retain) NSString *address;
+@property (nonatomic, retain) NSString *externalAddress;
+@property (nonatomic, retain) NSString *localAddress;
+@property (nonatomic, retain) NSString *mac;
+@property (nonatomic, retain) NSString *name;
+@property (nonatomic, retain) NSString *uuid;
+@property (nonatomic, retain) NSMutableSet *appList;
- (id) initFromHost:(Host*)host;
diff --git a/Limelight/Database/TemporaryHost.m b/Limelight/Database/TemporaryHost.m
index c71d7ab..31c4d98 100644
--- a/Limelight/Database/TemporaryHost.m
+++ b/Limelight/Database/TemporaryHost.m
@@ -8,7 +8,9 @@
#import "DataManager.h"
#import "TemporaryHost.h"
+#import "Host.h"
#import "TemporaryApp.h"
+#import "App.h"
@implementation TemporaryHost
@@ -16,7 +18,6 @@
self = [super init];
self.appList = [[NSMutableSet alloc] init];
self.currentGame = @"0";
- self.state = StateUnknown;
return self;
}
@@ -27,20 +28,10 @@
self.address = host.address;
self.externalAddress = host.externalAddress;
self.localAddress = host.localAddress;
- self.ipv6Address = host.ipv6Address;
self.mac = host.mac;
self.name = host.name;
self.uuid = host.uuid;
- self.serverCodecModeSupport = host.serverCodecModeSupport;
- self.serverCert = host.serverCert;
-
- // Older clients stored a non-URL-escaped IPv6 string. Try to detect that and fix it up.
- if (![self.ipv6Address containsString:@"["]) {
- self.ipv6Address = [Utils addressAndPortToAddressPortString:self.ipv6Address port:47989];
- }
-
- // Ensure we don't use a stale cached pair state if we haven't pinned the cert yet
- self.pairState = host.serverCert ? [host.pairState intValue] : PairStateUnpaired;
+ self.pairState = [host.pairState intValue];
NSMutableSet *appList = [[NSMutableSet alloc] init];
@@ -55,30 +46,12 @@
}
- (void) propagateChangesToParent:(Host*)parentHost {
- // Avoid overwriting existing data with nil if
- // we don't have everything populated in the temporary
- // host.
- if (self.address != nil) {
- parentHost.address = self.address;
- }
- if (self.externalAddress != nil) {
- parentHost.externalAddress = self.externalAddress;
- }
- if (self.localAddress != nil) {
- parentHost.localAddress = self.localAddress;
- }
- if (self.ipv6Address != nil) {
- parentHost.ipv6Address = self.ipv6Address;
- }
- if (self.mac != nil) {
- parentHost.mac = self.mac;
- }
- if (self.serverCert != nil) {
- parentHost.serverCert = self.serverCert;
- }
+ parentHost.address = self.address;
+ parentHost.externalAddress = self.externalAddress;
+ parentHost.localAddress = self.localAddress;
+ parentHost.mac = self.mac;
parentHost.name = self.name;
parentHost.uuid = self.uuid;
- parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
}
@@ -95,7 +68,7 @@
return YES;
}
- if (![object isKindOfClass:[self class]]) {
+ if (![object isKindOfClass:[Host class]]) {
return NO;
}
diff --git a/Limelight/Database/TemporarySettings.h b/Limelight/Database/TemporarySettings.h
index da35b30..2228912 100644
--- a/Limelight/Database/TemporarySettings.h
+++ b/Limelight/Database/TemporarySettings.h
@@ -6,7 +6,8 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
-#import "Settings+CoreDataClass.h"
+#import
+#import "Settings.h"
@interface TemporarySettings : NSObject
@@ -16,24 +17,8 @@
@property (nonatomic, retain) NSNumber * framerate;
@property (nonatomic, retain) NSNumber * height;
@property (nonatomic, retain) NSNumber * width;
-@property (nonatomic, retain) NSNumber * audioConfig;
@property (nonatomic, retain) NSNumber * onscreenControls;
@property (nonatomic, retain) NSString * uniqueId;
-@property (nonatomic) enum {
- CODEC_PREF_AUTO,
- CODEC_PREF_H264,
- CODEC_PREF_HEVC,
- CODEC_PREF_AV1,
-} preferredCodec;
-@property (nonatomic) BOOL useFramePacing;
-@property (nonatomic) BOOL multiController;
-@property (nonatomic) BOOL swapABXYButtons;
-@property (nonatomic) BOOL playAudioOnPC;
-@property (nonatomic) BOOL optimizeGames;
-@property (nonatomic) BOOL enableHdr;
-@property (nonatomic) BOOL btMouseSupport;
-@property (nonatomic) BOOL absoluteTouchMode;
-@property (nonatomic) BOOL statsOverlay;
- (id) initFromSettings:(Settings*)settings;
diff --git a/Limelight/Database/TemporarySettings.m b/Limelight/Database/TemporarySettings.m
index e627e0c..4b7437f 100644
--- a/Limelight/Database/TemporarySettings.m
+++ b/Limelight/Database/TemporarySettings.m
@@ -7,7 +7,6 @@
//
#import "TemporarySettings.h"
-#import "OnScreenControls.h"
@implementation TemporarySettings
@@ -16,76 +15,11 @@
self.parent = settings;
-#if TARGET_OS_TV
- // Apply default values from our Root.plist
- NSString* settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
- NSDictionary* settingsData = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
- NSArray* preferences = [settingsData objectForKey:@"PreferenceSpecifiers"];
- NSMutableDictionary* defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
- for (NSDictionary* prefSpecification in preferences) {
- NSString* key = [prefSpecification objectForKey:@"Key"];
- if (key != nil) {
- [defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
- }
- }
- [[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
-
- self.bitrate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"]];
- assert([self.bitrate intValue] != 0);
- self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
- assert([self.framerate intValue] != 0);
- self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]];
- assert([self.audioConfig intValue] != 0);
- self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
- self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
- self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
- self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
- self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
- self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
- self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
- self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
- self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
-
- NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
- switch (_screenSize) {
- case 0:
- self.height = [NSNumber numberWithInteger:720];
- self.width = [NSNumber numberWithInteger:1280];
- break;
- case 1:
- self.height = [NSNumber numberWithInteger:1080];
- self.width = [NSNumber numberWithInteger:1920];
- break;
- case 2:
- self.height = [NSNumber numberWithInteger:2160];
- self.width = [NSNumber numberWithInteger:3840];
- break;
- case 3:
- self.height = [NSNumber numberWithInteger:1440];
- self.width = [NSNumber numberWithInteger:2560];
- break;
- default:
- abort();
- }
- self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
-#else
self.bitrate = settings.bitrate;
self.framerate = settings.framerate;
self.height = settings.height;
self.width = settings.width;
- self.audioConfig = settings.audioConfig;
- self.preferredCodec = settings.preferredCodec;
- self.useFramePacing = settings.useFramePacing;
- self.playAudioOnPC = settings.playAudioOnPC;
- self.enableHdr = settings.enableHdr;
- self.optimizeGames = settings.optimizeGames;
- self.multiController = settings.multiController;
- self.swapABXYButtons = settings.swapABXYButtons;
self.onscreenControls = settings.onscreenControls;
- self.btMouseSupport = settings.btMouseSupport;
- self.absoluteTouchMode = settings.absoluteTouchMode;
- self.statsOverlay = settings.statsOverlay;
-#endif
self.uniqueId = settings.uniqueId;
return self;
diff --git a/Limelight/Font/Roboto/Apache License.txt b/Limelight/Font/Roboto/Apache License.txt
new file mode 100755
index 0000000..63d5302
--- /dev/null
+++ b/Limelight/Font/Roboto/Apache License.txt
@@ -0,0 +1,203 @@
+Font data copyright Google 2012
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
\ No newline at end of file
diff --git a/Limelight/Font/Roboto/Roboto-Black.ttf b/Limelight/Font/Roboto/Roboto-Black.ttf
new file mode 100755
index 0000000..71f01ac
Binary files /dev/null and b/Limelight/Font/Roboto/Roboto-Black.ttf differ
diff --git a/Limelight/Font/Roboto/Roboto-Bold.ttf b/Limelight/Font/Roboto/Roboto-Bold.ttf
new file mode 100755
index 0000000..aaf374d
Binary files /dev/null and b/Limelight/Font/Roboto/Roboto-Bold.ttf differ
diff --git a/Limelight/Font/Roboto/Roboto-Light.ttf b/Limelight/Font/Roboto/Roboto-Light.ttf
new file mode 100755
index 0000000..664e1b2
Binary files /dev/null and b/Limelight/Font/Roboto/Roboto-Light.ttf differ
diff --git a/Limelight/Font/Roboto/Roboto-Medium.ttf b/Limelight/Font/Roboto/Roboto-Medium.ttf
new file mode 100755
index 0000000..aa00de0
Binary files /dev/null and b/Limelight/Font/Roboto/Roboto-Medium.ttf differ
diff --git a/Limelight/Font/Roboto/Roboto-Regular.ttf b/Limelight/Font/Roboto/Roboto-Regular.ttf
new file mode 100755
index 0000000..3e6e2e7
Binary files /dev/null and b/Limelight/Font/Roboto/Roboto-Regular.ttf differ
diff --git a/Limelight/Font/Roboto/Roboto-Thin.ttf b/Limelight/Font/Roboto/Roboto-Thin.ttf
new file mode 100755
index 0000000..d262d14
Binary files /dev/null and b/Limelight/Font/Roboto/Roboto-Thin.ttf differ
diff --git a/Limelight/Images.xcassets/AddComputerIcon.imageset/Contents.json b/Limelight/Images.xcassets/AddComputerIcon.imageset/Contents.json
new file mode 100644
index 0000000..159ec2c
--- /dev/null
+++ b/Limelight/Images.xcassets/AddComputerIcon.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x",
+ "filename" : "limelight_computer_add_icon_1x.png"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x",
+ "filename" : "limelight_computer_add_icon_2x.png"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x",
+ "filename" : "limelight_computer_add_icon_3x.png"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_1x.png b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_1x.png
new file mode 100644
index 0000000..1c3d2db
Binary files /dev/null and b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_1x.png differ
diff --git a/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_2x.png b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_2x.png
new file mode 100644
index 0000000..661eddd
Binary files /dev/null and b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_2x.png differ
diff --git a/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_3x.png b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_3x.png
new file mode 100644
index 0000000..9323db1
Binary files /dev/null and b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_3x.png differ
diff --git a/Limelight/Images.xcassets/AddOverlayIcon.imageset/add.pdf b/Limelight/Images.xcassets/AddOverlayIcon.imageset/add.pdf
deleted file mode 100644
index 78b6f2f..0000000
--- a/Limelight/Images.xcassets/AddOverlayIcon.imageset/add.pdf
+++ /dev/null
@@ -1,68 +0,0 @@
-%PDF-1.5
-%µí®û
-4 0 obj
-<< /Length 5 0 R
- /Filter /FlateDecode
->>
-stream
-xœ]P[à ûÏ)r�1}°cìÒº�öcÛý¥AèZ!%q였°/ç"%Èè9oô&áz>_ž—/%qaä!ºyä�§T3ÐjqµîaÚ”t„,~Dû›ÖÚGHýôzQõ
®î:xgµ†I6Âß:gP(( hñ¯-1Îü5ç¶Žn3»!&úÖ"˜B-Õ¨~1–@½ZÒ¹](¡¿d÷ë9ا-žt§å…aL
-endstream
-endobj
-5 0 obj
- 183
-endobj
-3 0 obj
-<<
- /ExtGState <<
- /a0 << /CA 1 /ca 1 >>
- >>
->>
-endobj
-2 0 obj
-<< /Type /Page % 1
- /Parent 1 0 R
- /MediaBox [ 0 0 150 150 ]
- /Contents 4 0 R
- /Group <<
- /Type /Group
- /S /Transparency
- /I true
- /CS /DeviceRGB
- >>
- /Resources 3 0 R
->>
-endobj
-1 0 obj
-<< /Type /Pages
- /Kids [ 2 0 R ]
- /Count 1
->>
-endobj
-6 0 obj
-<< /Producer (cairo 1.16.0 (https://cairographics.org))
- /CreationDate (D:20190922201812-07'00)
->>
-endobj
-7 0 obj
-<< /Type /Catalog
- /Pages 1 0 R
->>
-endobj
-xref
-0 8
-0000000000 65535 f
-0000000587 00000 n
-0000000369 00000 n
-0000000297 00000 n
-0000000015 00000 n
-0000000275 00000 n
-0000000652 00000 n
-0000000768 00000 n
-trailer
-<< /Size 8
- /Root 7 0 R
- /Info 6 0 R
->>
-startxref
-820
-%%EOF
diff --git a/Limelight/Images.xcassets/AltIcon.imageset/AltIcon.png b/Limelight/Images.xcassets/AltIcon.imageset/AltIcon.png
deleted file mode 100644
index 222f643..0000000
Binary files a/Limelight/Images.xcassets/AltIcon.imageset/AltIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AltIcon.imageset/Contents.json b/Limelight/Images.xcassets/AltIcon.imageset/Contents.json
deleted file mode 100644
index 29a9ae7..0000000
--- a/Limelight/Images.xcassets/AltIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/1024x icon.png b/Limelight/Images.xcassets/AppIcon.appiconset/1024x icon.png
deleted file mode 100644
index 93eca38..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/1024x icon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Contents.json b/Limelight/Images.xcassets/AppIcon.appiconset/Contents.json
index 1a88ad3..1a6d102 100644
--- a/Limelight/Images.xcassets/AppIcon.appiconset/Contents.json
+++ b/Limelight/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -13,37 +13,37 @@
{
"size" : "29x29",
"idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
+ "filename" : "app_icon_settings_58x58_@2x-1.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
+ "filename" : "app_icon_settings_87x87_@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
+ "filename" : "app_icon_spotlight_80x80_@2x-1.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
+ "filename" : "app_icon_spotlight_120x120_@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
+ "filename" : "app_icon_120x120_@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
+ "filename" : "app_icon_180x180_@3x.png",
"scale" : "3x"
},
{
@@ -59,50 +59,44 @@
{
"size" : "29x29",
"idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
+ "filename" : "app_icon_settings_29x29_@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x-1.png",
+ "filename" : "app_icon_settings_58x58_@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
+ "filename" : "app_icon_spotlight_40x40_@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x-1.png",
+ "filename" : "app_icon_spotlight_80x80_@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
+ "filename" : "app_icon_76x76_@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
+ "filename" : "app_icon_152x152_@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
+ "filename" : "app_icon_167x167_@2x.png",
"scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "1024x icon.png",
- "scale" : "1x"
}
],
"info" : {
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 49a6d8c..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png
deleted file mode 100644
index 4b02770..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index 4b02770..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index 2d84c50..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index fa7bb64..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png
deleted file mode 100644
index 10d955b..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index 10d955b..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index 2bb7d5e..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index 2bb7d5e..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index 29bed7e..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index 958f7ae..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index c3c4861..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index 284e50a..0000000
Binary files a/Limelight/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_120x120_@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_120x120_@2x.png
new file mode 100644
index 0000000..5191e1e
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_120x120_@2x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_152x152_@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_152x152_@2x.png
new file mode 100644
index 0000000..cf22967
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_152x152_@2x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_167x167_@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_167x167_@2x.png
new file mode 100644
index 0000000..40619b0
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_167x167_@2x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_180x180_@3x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_180x180_@3x.png
new file mode 100644
index 0000000..23d5f09
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_180x180_@3x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_76x76_@1x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_76x76_@1x.png
new file mode 100644
index 0000000..4774683
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_76x76_@1x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_29x29_@1x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_29x29_@1x.png
new file mode 100644
index 0000000..4839e30
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_29x29_@1x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_58x58_@2x-1.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_58x58_@2x-1.png
new file mode 100644
index 0000000..4bf61e6
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_58x58_@2x-1.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_58x58_@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_58x58_@2x.png
new file mode 100644
index 0000000..4bf61e6
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_58x58_@2x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_87x87_@3x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_87x87_@3x.png
new file mode 100644
index 0000000..149f3d9
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_settings_87x87_@3x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_120x120_@3x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_120x120_@3x.png
new file mode 100644
index 0000000..5191e1e
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_120x120_@3x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_40x40_@1x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_40x40_@1x.png
new file mode 100644
index 0000000..5f7bdf5
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_40x40_@1x.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_80x80_@2x-1.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_80x80_@2x-1.png
new file mode 100644
index 0000000..8bd0845
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_80x80_@2x-1.png differ
diff --git a/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_80x80_@2x.png b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_80x80_@2x.png
new file mode 100644
index 0000000..8bd0845
Binary files /dev/null and b/Limelight/Images.xcassets/AppIcon.appiconset/app_icon_spotlight_80x80_@2x.png differ
diff --git a/Limelight/Images.xcassets/Computer.imageset/Computer.pdf b/Limelight/Images.xcassets/Computer.imageset/Computer.pdf
deleted file mode 100644
index 9aa1572..0000000
--- a/Limelight/Images.xcassets/Computer.imageset/Computer.pdf
+++ /dev/null
@@ -1,71 +0,0 @@
-%PDF-1.5
-%µí®û
-4 0 obj
-<< /Length 5 0 R
- /Filter /FlateDecode
->>
-stream
-xœ]‘[ƒ Eÿ³Šl )y¸Œ.¡ÃTû¡µûŸi€ Õa9×›�ÑȺ±l.zÌ+|ÀTm›ñþ48a”ä_°äÑ&&ë‘SØ^8cYÕÍÅíl"‹«Æ5ºá‚v¤�’"{
-ò<9¶�j�7Ö!Db ×,Ú�–PÊbåÐjt¤•øzÅÚœÔ:ƒº,'á½â‚ýÒ‡ wRÁù‘Ê|µ8)’w
--{GÍÑsµ™�æüÞG^’íosû»£MÅkäøà=h_
-endstream
-endobj
-5 0 obj
- 214
-endobj
-3 0 obj
-<<
- /ExtGState <<
- /a0 << /CA 0.3 /ca 0.3 >>
- /a1 << /CA 1 /ca 1 >>
- >>
->>
-endobj
-2 0 obj
-<< /Type /Page % 1
- /Parent 1 0 R
- /MediaBox [ 0 0 375 375 ]
- /Contents 4 0 R
- /Group <<
- /Type /Group
- /S /Transparency
- /I true
- /CS /DeviceRGB
- >>
- /Resources 3 0 R
->>
-endobj
-1 0 obj
-<< /Type /Pages
- /Kids [ 2 0 R ]
- /Count 1
->>
-endobj
-6 0 obj
-<< /Producer (cairo 1.16.0 (https://cairographics.org))
- /CreationDate (D:20190922201805-07'00)
->>
-endobj
-7 0 obj
-<< /Type /Catalog
- /Pages 1 0 R
->>
-endobj
-xref
-0 8
-0000000000 65535 f
-0000000650 00000 n
-0000000432 00000 n
-0000000328 00000 n
-0000000015 00000 n
-0000000306 00000 n
-0000000715 00000 n
-0000000831 00000 n
-trailer
-<< /Size 8
- /Root 7 0 R
- /Info 6 0 R
->>
-startxref
-883
-%%EOF
diff --git a/Limelight/Images.xcassets/Computer.imageset/Contents.json b/Limelight/Images.xcassets/Computer.imageset/Contents.json
index ef963c3..3a06eed 100644
--- a/Limelight/Images.xcassets/Computer.imageset/Contents.json
+++ b/Limelight/Images.xcassets/Computer.imageset/Contents.json
@@ -2,16 +2,18 @@
"images" : [
{
"idiom" : "universal",
- "filename" : "Computer.pdf",
- "scale" : "1x"
+ "scale" : "1x",
+ "filename" : "limelight_computer_1x.png"
},
{
"idiom" : "universal",
- "scale" : "2x"
+ "scale" : "2x",
+ "filename" : "limelight_computer_2x.png"
},
{
"idiom" : "universal",
- "scale" : "3x"
+ "scale" : "3x",
+ "filename" : "limelight_computer_3x.png"
}
],
"info" : {
diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_1x.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_1x.png
new file mode 100644
index 0000000..7c2e71f
Binary files /dev/null and b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_1x.png differ
diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png
new file mode 100644
index 0000000..25468de
Binary files /dev/null and b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png differ
diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_3x.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_3x.png
new file mode 100644
index 0000000..e93d417
Binary files /dev/null and b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_3x.png differ
diff --git a/Limelight/Images.xcassets/Contents.json b/Limelight/Images.xcassets/Contents.json
index 73c0059..da4a164 100644
--- a/Limelight/Images.xcassets/Contents.json
+++ b/Limelight/Images.xcassets/Contents.json
@@ -1,6 +1,6 @@
{
"info" : {
- "author" : "xcode",
- "version" : 1
+ "version" : 1,
+ "author" : "xcode"
}
-}
+}
\ No newline at end of file
diff --git a/Limelight/Images.xcassets/ControlIcon.imageset/Contents.json b/Limelight/Images.xcassets/ControlIcon.imageset/Contents.json
deleted file mode 100644
index 24227b3..0000000
--- a/Limelight/Images.xcassets/ControlIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/ControlIcon.imageset/ControlIcon.png b/Limelight/Images.xcassets/ControlIcon.imageset/ControlIcon.png
deleted file mode 100644
index 48433dd..0000000
Binary files a/Limelight/Images.xcassets/ControlIcon.imageset/ControlIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/DeleteIcon.imageset/Contents.json b/Limelight/Images.xcassets/DeleteIcon.imageset/Contents.json
deleted file mode 100644
index 09d0484..0000000
--- a/Limelight/Images.xcassets/DeleteIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/DeleteIcon.imageset/DeleteIcon.png b/Limelight/Images.xcassets/DeleteIcon.imageset/DeleteIcon.png
deleted file mode 100644
index 3a3e323..0000000
Binary files a/Limelight/Images.xcassets/DeleteIcon.imageset/DeleteIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/DoneIcon.imageset/Contents.json b/Limelight/Images.xcassets/DoneIcon.imageset/Contents.json
deleted file mode 100644
index 9229c64..0000000
--- a/Limelight/Images.xcassets/DoneIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/DoneIcon.imageset/doneIcon.png b/Limelight/Images.xcassets/DoneIcon.imageset/doneIcon.png
deleted file mode 100644
index f4d178f..0000000
Binary files a/Limelight/Images.xcassets/DoneIcon.imageset/doneIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/ErrorOverlayIcon.imageset/error.pdf b/Limelight/Images.xcassets/ErrorOverlayIcon.imageset/error.pdf
deleted file mode 100644
index f024de8..0000000
--- a/Limelight/Images.xcassets/ErrorOverlayIcon.imageset/error.pdf
+++ /dev/null
@@ -1,68 +0,0 @@
-%PDF-1.5
-%µí®û
-4 0 obj
-<< /Length 5 0 R
- /Filter /FlateDecode
->>
-stream
-xœUMA€ »ïý€¸� ßð ÆD=ÈAý"L4š&[»µ›€3ÉE>
- >>
->>
-endobj
-2 0 obj
-<< /Type /Page % 1
- /Parent 1 0 R
- /MediaBox [ 0 0 150 150 ]
- /Contents 4 0 R
- /Group <<
- /Type /Group
- /S /Transparency
- /I true
- /CS /DeviceRGB
- >>
- /Resources 3 0 R
->>
-endobj
-1 0 obj
-<< /Type /Pages
- /Kids [ 2 0 R ]
- /Count 1
->>
-endobj
-6 0 obj
-<< /Producer (cairo 1.16.0 (https://cairographics.org))
- /CreationDate (D:20190922201819-07'00)
->>
-endobj
-7 0 obj
-<< /Type /Catalog
- /Pages 1 0 R
->>
-endobj
-xref
-0 8
-0000000000 65535 f
-0000000514 00000 n
-0000000296 00000 n
-0000000224 00000 n
-0000000015 00000 n
-0000000202 00000 n
-0000000579 00000 n
-0000000695 00000 n
-trailer
-<< /Size 8
- /Root 7 0 R
- /Info 6 0 R
->>
-startxref
-747
-%%EOF
diff --git a/Limelight/Images.xcassets/EscapeIcon.imageset/Contents.json b/Limelight/Images.xcassets/EscapeIcon.imageset/Contents.json
deleted file mode 100644
index 5533897..0000000
--- a/Limelight/Images.xcassets/EscapeIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/EscapeIcon.imageset/EscapeIcon.png b/Limelight/Images.xcassets/EscapeIcon.imageset/EscapeIcon.png
deleted file mode 100644
index 262f024..0000000
Binary files a/Limelight/Images.xcassets/EscapeIcon.imageset/EscapeIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/L2.imageset/Contents.json b/Limelight/Images.xcassets/L2.imageset/Contents.json
index edd9020..b2bba89 100644
--- a/Limelight/Images.xcassets/L2.imageset/Contents.json
+++ b/Limelight/Images.xcassets/L2.imageset/Contents.json
@@ -2,17 +2,18 @@
"images" : [
{
"idiom" : "universal",
- "filename" : "L2_60x60@1x.png",
- "scale" : "1x"
+ "scale" : "1x",
+ "filename" : "L2_60x60@1x.png"
},
{
"idiom" : "universal",
- "filename" : "L2_120x120@2x.png",
- "scale" : "2x"
+ "scale" : "2x",
+ "filename" : "L2_120x120@2x.png"
},
{
"idiom" : "universal",
- "scale" : "3x"
+ "scale" : "3x",
+ "filename" : "L2_180x180@3x.png"
}
],
"info" : {
diff --git a/Limelight/Images.xcassets/L2.imageset/L2_180x180@3x.png b/Limelight/Images.xcassets/L2.imageset/L2_180x180@3x.png
new file mode 100644
index 0000000..456589c
Binary files /dev/null and b/Limelight/Images.xcassets/L2.imageset/L2_180x180@3x.png differ
diff --git a/Limelight/Images.xcassets/LockedOverlayIcon.imageset/Contents.json b/Limelight/Images.xcassets/LockedOverlayIcon.imageset/Contents.json
deleted file mode 100644
index cdaf694..0000000
--- a/Limelight/Images.xcassets/LockedOverlayIcon.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "lock.pdf",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Limelight/Images.xcassets/LockedOverlayIcon.imageset/lock.pdf b/Limelight/Images.xcassets/LockedOverlayIcon.imageset/lock.pdf
deleted file mode 100644
index fe5b737..0000000
Binary files a/Limelight/Images.xcassets/LockedOverlayIcon.imageset/lock.pdf and /dev/null differ
diff --git a/Limelight/Images.xcassets/AddOverlayIcon.imageset/Contents.json b/Limelight/Images.xcassets/Logo.imageset/Contents.json
similarity index 80%
rename from Limelight/Images.xcassets/AddOverlayIcon.imageset/Contents.json
rename to Limelight/Images.xcassets/Logo.imageset/Contents.json
index 813ddd3..fa1e947 100644
--- a/Limelight/Images.xcassets/AddOverlayIcon.imageset/Contents.json
+++ b/Limelight/Images.xcassets/Logo.imageset/Contents.json
@@ -2,12 +2,12 @@
"images" : [
{
"idiom" : "universal",
- "filename" : "add.pdf",
"scale" : "1x"
},
{
"idiom" : "universal",
- "scale" : "2x"
+ "scale" : "2x",
+ "filename" : "logo_universal.png"
},
{
"idiom" : "universal",
diff --git a/Limelight/Images.xcassets/Logo.imageset/logo_universal.png b/Limelight/Images.xcassets/Logo.imageset/logo_universal.png
new file mode 100644
index 0000000..eb39b82
Binary files /dev/null and b/Limelight/Images.xcassets/Logo.imageset/logo_universal.png differ
diff --git a/Limelight/Images.xcassets/ErrorOverlayIcon.imageset/Contents.json b/Limelight/Images.xcassets/PullArrow.imageset/Contents.json
similarity index 63%
rename from Limelight/Images.xcassets/ErrorOverlayIcon.imageset/Contents.json
rename to Limelight/Images.xcassets/PullArrow.imageset/Contents.json
index 66c6dba..3e235a1 100644
--- a/Limelight/Images.xcassets/ErrorOverlayIcon.imageset/Contents.json
+++ b/Limelight/Images.xcassets/PullArrow.imageset/Contents.json
@@ -2,15 +2,17 @@
"images" : [
{
"idiom" : "universal",
- "filename" : "error.pdf",
+ "filename" : "pull-arrow-light-14x31@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
+ "filename" : "pull-arrow-light-28x61@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
+ "filename" : "pull-arrow-light-42x92@3x.png",
"scale" : "3x"
}
],
diff --git a/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-14x31@1x.png b/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-14x31@1x.png
new file mode 100644
index 0000000..6a3a11b
Binary files /dev/null and b/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-14x31@1x.png differ
diff --git a/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-28x61@2x.png b/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-28x61@2x.png
new file mode 100644
index 0000000..383b416
Binary files /dev/null and b/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-28x61@2x.png differ
diff --git a/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-42x92@3x.png b/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-42x92@3x.png
new file mode 100644
index 0000000..e701942
Binary files /dev/null and b/Limelight/Images.xcassets/PullArrow.imageset/pull-arrow-light-42x92@3x.png differ
diff --git a/Limelight/Images.xcassets/R2.imageset/Contents.json b/Limelight/Images.xcassets/R2.imageset/Contents.json
index 0c1a895..bbaf1d6 100644
--- a/Limelight/Images.xcassets/R2.imageset/Contents.json
+++ b/Limelight/Images.xcassets/R2.imageset/Contents.json
@@ -2,17 +2,18 @@
"images" : [
{
"idiom" : "universal",
- "filename" : "R2_60x60@1x.png",
- "scale" : "1x"
+ "scale" : "1x",
+ "filename" : "R2_60x60@1x.png"
},
{
"idiom" : "universal",
- "filename" : "R2_120x120@2x.png",
- "scale" : "2x"
+ "scale" : "2x",
+ "filename" : "R2_120x120@2x.png"
},
{
"idiom" : "universal",
- "scale" : "3x"
+ "scale" : "3x",
+ "filename" : "R2_120x120@3x.png"
}
],
"info" : {
diff --git a/Limelight/Images.xcassets/R2.imageset/R2_120x120@3x.png b/Limelight/Images.xcassets/R2.imageset/R2_120x120@3x.png
new file mode 100644
index 0000000..51a40e5
Binary files /dev/null and b/Limelight/Images.xcassets/R2.imageset/R2_120x120@3x.png differ
diff --git a/Limelight/Images.xcassets/ShiftIcon.imageset/Contents.json b/Limelight/Images.xcassets/ShiftIcon.imageset/Contents.json
deleted file mode 100644
index 12d9131..0000000
--- a/Limelight/Images.xcassets/ShiftIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/ShiftIcon.imageset/ShiftIcon.png b/Limelight/Images.xcassets/ShiftIcon.imageset/ShiftIcon.png
deleted file mode 100644
index 3e33f4a..0000000
Binary files a/Limelight/Images.xcassets/ShiftIcon.imageset/ShiftIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/TabIcon.imageset/Contents.json b/Limelight/Images.xcassets/TabIcon.imageset/Contents.json
deleted file mode 100644
index fd7d96e..0000000
--- a/Limelight/Images.xcassets/TabIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/TabIcon.imageset/TabIcon.png b/Limelight/Images.xcassets/TabIcon.imageset/TabIcon.png
deleted file mode 100644
index f562b1d..0000000
Binary files a/Limelight/Images.xcassets/TabIcon.imageset/TabIcon.png and /dev/null differ
diff --git a/Limelight/Images.xcassets/UpIcon.imageset/Contents.json b/Limelight/Images.xcassets/UpIcon.imageset/Contents.json
deleted file mode 100644
index 0f0fff7..0000000
--- a/Limelight/Images.xcassets/UpIcon.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "universal",
- "filename" : "up_arrow.pdf",
- "scale" : "1x"
- },
- {
- "idiom" : "universal",
- "scale" : "2x"
- },
- {
- "idiom" : "universal",
- "scale" : "3x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Limelight/Images.xcassets/UpIcon.imageset/up_arrow.pdf b/Limelight/Images.xcassets/UpIcon.imageset/up_arrow.pdf
deleted file mode 100644
index 1fa5a70..0000000
--- a/Limelight/Images.xcassets/UpIcon.imageset/up_arrow.pdf
+++ /dev/null
@@ -1,70 +0,0 @@
-%PDF-1.5
-%µí®û
-4 0 obj
-<< /Length 5 0 R
- /Filter /FlateDecode
->>
-stream
-xœUŽQ
-ƒ@Dÿsй€1ÙÍF=†G(…êÇú¡½?-e`ƒÌ$
- 5Ö¼ZIqh›Ð¾Ó—4qAî¨ó ÓsTdã¹dl>üØ�H… {º‰n|–KÏâé,Ë}°?‡ê5ôª™ÿ^øÐH;U$}
-endstream
-endobj
-5 0 obj
- 114
-endobj
-3 0 obj
-<<
- /ExtGState <<
- /a0 << /CA 1 /ca 1 >>
- >>
->>
-endobj
-2 0 obj
-<< /Type /Page % 1
- /Parent 1 0 R
- /MediaBox [ 0 0 75 75 ]
- /Contents 4 0 R
- /Group <<
- /Type /Group
- /S /Transparency
- /I true
- /CS /DeviceRGB
- >>
- /Resources 3 0 R
->>
-endobj
-1 0 obj
-<< /Type /Pages
- /Kids [ 2 0 R ]
- /Count 1
->>
-endobj
-6 0 obj
-<< /Producer (cairo 1.16.0 (https://cairographics.org))
- /CreationDate (D:20190830203401-07'00)
->>
-endobj
-7 0 obj
-<< /Type /Catalog
- /Pages 1 0 R
->>
-endobj
-xref
-0 8
-0000000000 65535 f
-0000000516 00000 n
-0000000300 00000 n
-0000000228 00000 n
-0000000015 00000 n
-0000000206 00000 n
-0000000581 00000 n
-0000000697 00000 n
-trailer
-<< /Size 8
- /Root 7 0 R
- /Info 6 0 R
->>
-startxref
-749
-%%EOF
diff --git a/Limelight/Images.xcassets/WindowsIcon.imageset/Contents.json b/Limelight/Images.xcassets/WindowsIcon.imageset/Contents.json
deleted file mode 100644
index 778eea3..0000000
--- a/Limelight/Images.xcassets/WindowsIcon.imageset/Contents.json
+++ /dev/null
@@ -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
- }
-}
diff --git a/Limelight/Images.xcassets/WindowsIcon.imageset/WindowsIcon.png b/Limelight/Images.xcassets/WindowsIcon.imageset/WindowsIcon.png
deleted file mode 100644
index 60dec54..0000000
Binary files a/Limelight/Images.xcassets/WindowsIcon.imageset/WindowsIcon.png and /dev/null differ
diff --git a/Limelight/Input/AbsoluteTouchHandler.h b/Limelight/Input/AbsoluteTouchHandler.h
deleted file mode 100644
index cbdb455..0000000
--- a/Limelight/Input/AbsoluteTouchHandler.h
+++ /dev/null
@@ -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
diff --git a/Limelight/Input/AbsoluteTouchHandler.m b/Limelight/Input/AbsoluteTouchHandler.m
deleted file mode 100644
index b9b8c59..0000000
--- a/Limelight/Input/AbsoluteTouchHandler.m
+++ /dev/null
@@ -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
-
-// 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
diff --git a/Limelight/Input/Controller.h b/Limelight/Input/Controller.h
deleted file mode 100644
index 6e4511c..0000000
--- a/Limelight/Input/Controller.h
+++ /dev/null
@@ -1,53 +0,0 @@
-//
-// Controller.h
-// Moonlight
-//
-// Created by Cameron Gutman on 2/11/19.
-// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
-//
-
-#import "HapticContext.h"
-
-@import GameController;
-@import CoreHaptics;
-
-@interface Controller : NSObject
-
-typedef struct {
- float lastX;
- float lastY;
-} controller_touch_context_t;
-
-@property (nullable, nonatomic, retain) GCController* gamepad;
-@property (nonatomic) int playerIndex;
-@property (nonatomic) int lastButtonFlags;
-@property (nonatomic) int emulatingButtonFlags;
-@property (nonatomic) int supportedEmulationFlags;
-@property (nonatomic) unsigned char lastLeftTrigger;
-@property (nonatomic) unsigned char lastRightTrigger;
-@property (nonatomic) short lastLeftStickX;
-@property (nonatomic) short lastLeftStickY;
-@property (nonatomic) short lastRightStickX;
-@property (nonatomic) short lastRightStickY;
-
-@property (nonatomic) controller_touch_context_t primaryTouch;
-@property (nonatomic) controller_touch_context_t secondaryTouch;
-
-@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
-@property (nonatomic) HapticContext* _Nullable highFreqMotor;
-@property (nonatomic) HapticContext* _Nullable leftTriggerMotor;
-@property (nonatomic) HapticContext* _Nullable rightTriggerMotor;
-
-@property (nonatomic) NSTimer* _Nullable accelTimer;
-@property (nonatomic) GCAcceleration lastAccelSample;
-@property (nonatomic) NSTimer* _Nullable gyroTimer;
-@property (nonatomic) GCRotationRate lastGyroSample;
-
-@property (nonatomic) NSTimer* _Nullable batteryTimer;
-@property (nonatomic) GCDeviceBatteryState lastBatteryState;
-@property (nonatomic) float lastBatteryLevel;
-
-@property (nonatomic) BOOL reportedArrival;
-@property (nonatomic) Controller* _Nullable mergedWithController;
-
-@end
diff --git a/Limelight/Input/Controller.m b/Limelight/Input/Controller.m
deleted file mode 100644
index 7587928..0000000
--- a/Limelight/Input/Controller.m
+++ /dev/null
@@ -1,13 +0,0 @@
-//
-// Controller.m
-// Moonlight
-//
-// Created by Cameron Gutman on 2/11/19.
-// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
-//
-
-#include "Controller.h"
-
-@implementation Controller
-
-@end
diff --git a/Limelight/Input/Controller.swift b/Limelight/Input/Controller.swift
new file mode 100644
index 0000000..9e3d86c
--- /dev/null
+++ b/Limelight/Input/Controller.swift
@@ -0,0 +1,26 @@
+//
+// Controller.swift
+// Moonlight
+//
+// Created by David Aghassi on 4/11/16.
+// Copyright © 2016 Moonlight Stream. All rights reserved.
+//
+
+import Foundation
+
+@objc
+/**
+ Defines a controller layout
+ */
+class Controller: NSObject {
+ // Swift requires initial properties
+ var playerIndex: CInt = 0 // Controller number (e.g. 1, 2 ,3 etc)
+ var lastButtonFlags: CInt = 0
+ var emulatingButtonFlags: CInt = 0
+ var lastLeftTrigger: CChar = 0 // Last left trigger pressed
+ var lastRightTrigger: CChar = 0 // Last right trigger pressed
+ var lastLeftStickX: CShort = 0 // Last X direction the left joystick went
+ var lastLeftStickY: CShort = 0 // Last Y direction the left joystick went
+ var lastRightStickX: CShort = 0 // Last X direction the right joystick went
+ var lastRightStickY: CShort = 0 // Last Y direction the right joystick went
+}
diff --git a/Limelight/Input/ControllerSupport.h b/Limelight/Input/ControllerSupport.h
index a805c67..d6432f8 100644
--- a/Limelight/Input/ControllerSupport.h
+++ b/Limelight/Input/ControllerSupport.h
@@ -6,48 +6,35 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
-#import "StreamConfiguration.h"
-#import "Controller.h"
+#import
+
+// Swift
+#import "Moonlight-Swift.h"
+@class Controller;
@class OnScreenControls;
-@protocol ControllerSupportDelegate
-
-- (void) gamepadPresenceChanged;
-- (void) mousePresenceChanged;
-- (void) streamExitRequested;
-
-@end
-
@interface ControllerSupport : NSObject
--(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id)delegate;
--(void) connectionEstablished;
-
+-(id) init;
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
-(void) cleanup;
--(Controller*) getOscController;
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
--(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left;
--(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right;
--(void) updateTriggers:(Controller*)controller left:(unsigned char)left right:(unsigned char)right;
+-(void) updateLeftTrigger:(Controller*)controller left:(char)left;
+-(void) updateRightTrigger:(Controller*)controller right:(char)right;
+-(void) updateTriggers:(Controller*)controller left:(char)left right:(char)right;
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags;
-(void) setButtonFlag:(Controller*)controller flags:(int)flags;
-(void) clearButtonFlag:(Controller*)controller flags:(int)flags;
-(void) updateFinished:(Controller*)controller;
+-(Controller*) getOscController;
--(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;
-
--(NSUInteger) getConnectedGamepadCount;
+@property (nonatomic, strong) id connectObserver;
+@property (nonatomic, strong) id disconnectObserver;
@end
diff --git a/Limelight/Input/ControllerSupport.m b/Limelight/Input/ControllerSupport.m
index 19d1315..14960e4 100644
--- a/Limelight/Input/ControllerSupport.m
+++ b/Limelight/Input/ControllerSupport.m
@@ -7,188 +7,35 @@
//
#import "ControllerSupport.h"
-#import "Controller.h"
-
#import "OnScreenControls.h"
-
-#import "DataManager.h"
#include "Limelight.h"
-@import GameController;
-@import AudioToolbox;
+// Swift
+#import "Moonlight-Swift.h"
+@class Controller;
-static const double MOUSE_SPEED_DIVISOR = 1.25;
+@import GameController;
@implementation ControllerSupport {
- id _controllerConnectObserver;
- id _controllerDisconnectObserver;
- id _mouseConnectObserver;
- id _mouseDisconnectObserver;
- id _keyboardConnectObserver;
- id _keyboardDisconnectObserver;
-
NSLock *_controllerStreamLock;
NSMutableDictionary *_controllers;
- id _delegate;
-
- float accumulatedDeltaX;
- float accumulatedDeltaY;
- float accumulatedScrollX;
- float accumulatedScrollY;
OnScreenControls *_osc;
- Controller *_oscController;
+
+ // This controller object is shared between on-screen controls
+ // and player 0
+ Controller *_player0osc;
+
+ char _controllerNumbers;
#define EMULATING_SELECT 0x1
#define EMULATING_SPECIAL 0x2
-
- bool _oscEnabled;
- char _controllerNumbers;
- bool _multiController;
- bool _swapABXYButtons;
}
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
#define UPDATE_BUTTON_FLAG(controller, x, y) \
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
-#define MAX_MAGNITUDE(x, y) (abs(x) > abs(y) ? (x) : (y))
-
--(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
-{
- Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
- if (controller == nil && controllerNumber == 0 && _oscEnabled) {
- // TODO: Rumble emulation for OSC
- }
- if (controller == nil) {
- // No connected controller for this player
- return;
- }
-
- [controller.lowFreqMotor setMotorAmplitude:lowFreqMotor];
- [controller.highFreqMotor setMotorAmplitude:highFreqMotor];
-}
-
--(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger
-{
- Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
- if (controller == nil && controllerNumber == 0 && _oscEnabled) {
- // TODO: Trigger rumble emulation for OSC
- }
- if (controller == nil) {
- // No connected controller for this player
- return;
- }
-
- [controller.leftTriggerMotor setMotorAmplitude:leftTrigger];
- [controller.rightTriggerMotor setMotorAmplitude:rightTrigger];
-}
-
-- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz
-{
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
- if (controller == nil) {
- // No connected controller for this player
- return;
- }
-
- if (controller.gamepad.motion == nil) {
- // No motion supported for this controller
- return;
- }
-
- switch (motionType) {
- case LI_MOTION_TYPE_ACCEL:
- [controller.accelTimer invalidate];
- controller.accelTimer = nil;
-
- if (reportRateHz && controller.gamepad.motion.hasGravityAndUserAcceleration) {
- // Reset the last motion sample
- GCAcceleration emptyAccelSample = {};
- controller.lastAccelSample = emptyAccelSample;
-
- dispatch_sync(dispatch_get_main_queue(), ^{
- controller.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / reportRateHz repeats:YES block:^(NSTimer *timer) {
- // Don't send duplicate samples
- GCAcceleration lastAccelSample = controller.lastAccelSample;
- GCAcceleration accelSample = controller.gamepad.motion.acceleration;
- if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) {
- return;
- }
- controller.lastAccelSample = accelSample;
-
- // Convert g to m/s^2
- LiSendControllerMotionEvent((uint8_t)controllerNumber,
- LI_MOTION_TYPE_ACCEL,
- accelSample.x * -9.80665f,
- accelSample.y * -9.80665f,
- accelSample.z * -9.80665f);
- }];
- });
- }
- break;
-
- case LI_MOTION_TYPE_GYRO:
- [controller.gyroTimer invalidate];
- controller.gyroTimer = nil;
-
- if (reportRateHz && controller.gamepad.motion.hasRotationRate) {
- // Reset the last motion sample
- GCRotationRate emptyGyroSample = {};
- controller.lastGyroSample = emptyGyroSample;
-
- dispatch_sync(dispatch_get_main_queue(), ^{
- controller.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / reportRateHz repeats:YES block:^(NSTimer *timer) {
- // Don't send duplicate samples
- GCRotationRate lastGyroSample = controller.lastGyroSample;
- GCRotationRate gyroSample = controller.gamepad.motion.rotationRate;
- if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) {
- return;
- }
- controller.lastGyroSample = gyroSample;
-
- // Convert rad/s to deg/s
- LiSendControllerMotionEvent((uint8_t)controllerNumber,
- LI_MOTION_TYPE_GYRO,
- gyroSample.x * 57.2957795f,
- gyroSample.z * 57.2957795f,
- gyroSample.y * -57.2957795f);
- }];
- });
- }
- break;
- }
-
- // Set the motion sensor state if they require manual activation
- if (controller.gamepad.motion.sensorsRequireManualActivation) {
- if (controller.gyroTimer || controller.accelTimer) {
- controller.gamepad.motion.sensorsActive = YES;
- }
- else {
- controller.gamepad.motion.sensorsActive = NO;
- }
- }
- }
-}
-
--(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
- if (controller == nil) {
- // No connected controller for this player
- return;
- }
-
- if (controller.gamepad.light == nil) {
- // No LED control supported for this controller
- return;
- }
-
- controller.gamepad.light.color = [[GCColor alloc] initWithRed:(r / 255.0f) green:(g / 255.0f) blue:(b / 255.0f)];
- }
-}
-
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
{
@synchronized(controller) {
@@ -205,21 +52,21 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
}
}
--(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left
+-(void) updateLeftTrigger:(Controller*)controller left:(char)left
{
@synchronized(controller) {
controller.lastLeftTrigger = left;
}
}
--(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right
+-(void) updateRightTrigger:(Controller*)controller right:(char)right
{
@synchronized(controller) {
controller.lastRightTrigger = right;
}
}
--(void) updateTriggers:(Controller*) controller left:(unsigned char)left right:(unsigned char)right
+-(void) updateTriggers:(Controller*) controller left:(char)left right:(char)right
{
@synchronized(controller) {
controller.lastLeftTrigger = left;
@@ -229,26 +76,17 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
-(void) handleSpecialCombosReleased:(Controller*)controller releasedButtons:(int)releasedButtons
{
- if ((controller.emulatingButtonFlags & EMULATING_SELECT) && (releasedButtons & (LB_FLAG | PLAY_FLAG))) {
+ if ((controller.emulatingButtonFlags & EMULATING_SELECT) &&
+ ((releasedButtons & LB_FLAG) || (releasedButtons & PLAY_FLAG))) {
controller.lastButtonFlags &= ~BACK_FLAG;
controller.emulatingButtonFlags &= ~EMULATING_SELECT;
}
-
- if (controller.emulatingButtonFlags & EMULATING_SPECIAL) {
- // If Select is emulated, we use RB+Start to emulate special, otherwise we use Start+Select
- if (controller.supportedEmulationFlags & EMULATING_SELECT) {
- if (releasedButtons & (RB_FLAG | PLAY_FLAG)) {
- controller.lastButtonFlags &= ~SPECIAL_FLAG;
- controller.emulatingButtonFlags &= ~EMULATING_SPECIAL;
- }
+ if ((controller.emulatingButtonFlags & EMULATING_SPECIAL) &&
+ ((releasedButtons & RB_FLAG) || (releasedButtons & PLAY_FLAG) ||
+ (releasedButtons & BACK_FLAG))) {
+ controller.lastButtonFlags &= ~SPECIAL_FLAG;
+ controller.emulatingButtonFlags &= ~EMULATING_SPECIAL;
}
- else {
- if (releasedButtons & (BACK_FLAG | PLAY_FLAG)) {
- controller.lastButtonFlags &= ~SPECIAL_FLAG;
- controller.emulatingButtonFlags &= ~EMULATING_SPECIAL;
- }
- }
- }
}
-(void) handleSpecialCombosPressed:(Controller*)controller pressedButtons:(int)pressedButtons
@@ -257,46 +95,34 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
if (controller.lastButtonFlags & PLAY_FLAG) {
// If LB and start are down, trigger select
if (controller.lastButtonFlags & LB_FLAG) {
- if (controller.supportedEmulationFlags & EMULATING_SELECT) {
- controller.lastButtonFlags |= BACK_FLAG;
- controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG));
- controller.emulatingButtonFlags |= EMULATING_SELECT;
- }
+ controller.lastButtonFlags |= BACK_FLAG;
+ controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG));
+ controller.emulatingButtonFlags |= EMULATING_SELECT;
}
- else if (controller.supportedEmulationFlags & EMULATING_SPECIAL) {
- // If Select is emulated too, use RB+Start to emulate special
- if (controller.supportedEmulationFlags & EMULATING_SELECT) {
- if (controller.lastButtonFlags & RB_FLAG) {
- controller.lastButtonFlags |= SPECIAL_FLAG;
- controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG));
- controller.emulatingButtonFlags |= EMULATING_SPECIAL;
- }
- }
- else {
- // If Select is physical, use Start+Select to emulate special
- if (controller.lastButtonFlags & BACK_FLAG) {
- controller.lastButtonFlags |= SPECIAL_FLAG;
- controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | BACK_FLAG));
- controller.emulatingButtonFlags |= EMULATING_SPECIAL;
- }
- }
+ // If (RB or select) and start are down, trigger special
+ else if ((controller.lastButtonFlags & RB_FLAG) || (controller.lastButtonFlags & BACK_FLAG)) {
+ controller.lastButtonFlags |= SPECIAL_FLAG;
+ controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG | BACK_FLAG));
+ controller.emulatingButtonFlags |= EMULATING_SPECIAL;
}
}
+
}
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags
{
@synchronized(controller) {
+ int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
+ int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
+
controller.lastButtonFlags = flags;
// This must be called before handleSpecialCombosPressed
// because we clear the original button flags there
- int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
- int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
-
[self handleSpecialCombosReleased:controller releasedButtons:releasedButtons];
[self handleSpecialCombosPressed:controller pressedButtons:pressedButtons];
+
}
}
@@ -316,428 +142,55 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
}
}
--(uint16_t) getActiveGamepadMask
-{
- return (_multiController ? _controllerNumbers : 1) | (_oscEnabled ? 1 : 0);
-}
-
-(void) updateFinished:(Controller*)controller
{
- BOOL exitRequested = NO;
-
[_controllerStreamLock lock];
@synchronized(controller) {
- // Handle Start+Select+L1+R1 gamepad quit combo
- if (controller.lastButtonFlags == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
- controller.lastButtonFlags = 0;
- exitRequested = YES;
- }
-
- // Only send controller events if we successfully reported controller arrival
- if ([self reportControllerArrival:controller]) {
- uint32_t buttonFlags = controller.lastButtonFlags;
- uint8_t leftTrigger = controller.lastLeftTrigger;
- uint8_t rightTrigger = controller.lastRightTrigger;
- int16_t leftStickX = controller.lastLeftStickX;
- int16_t leftStickY = controller.lastLeftStickY;
- int16_t rightStickX = controller.lastRightStickX;
- int16_t rightStickY = controller.lastRightStickY;
-
- // If this is merged with another controller, combine the inputs
- if (controller.mergedWithController) {
- buttonFlags |= controller.mergedWithController.lastButtonFlags;
- leftTrigger = MAX(leftTrigger, controller.mergedWithController.lastLeftTrigger);
- rightTrigger = MAX(rightTrigger, controller.mergedWithController.lastRightTrigger);
- leftStickX = MAX_MAGNITUDE(leftStickX, controller.mergedWithController.lastLeftStickX);
- leftStickY = MAX_MAGNITUDE(leftStickY, controller.mergedWithController.lastLeftStickY);
- rightStickX = MAX_MAGNITUDE(rightStickX, controller.mergedWithController.lastRightStickX);
- rightStickY = MAX_MAGNITUDE(rightStickY, controller.mergedWithController.lastRightStickY);
- }
-
- // Player 1 is always present for OSC
- LiSendMultiControllerEvent(_multiController ? controller.playerIndex : 0, [self getActiveGamepadMask],
- buttonFlags, leftTrigger, rightTrigger,
- leftStickX, leftStickY, rightStickX, rightStickY);
- }
+ LiSendMultiControllerEvent(controller.playerIndex, controller.lastButtonFlags, controller.lastLeftTrigger, controller.lastRightTrigger, controller.lastLeftStickX, controller.lastLeftStickY, controller.lastRightStickX, controller.lastRightStickY);
}
[_controllerStreamLock unlock];
-
- if (exitRequested) {
- // Invoke the delegate callback on the main thread
- dispatch_async(dispatch_get_main_queue(), ^{
- [self->_delegate streamExitRequested];
- });
- }
}
-+(BOOL) hasKeyboardOrMouse {
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- return GCMouse.mice.count > 0 || GCKeyboard.coalescedKeyboard != nil;
- }
- else {
- return NO;
- }
-}
-
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-
-(void) unregisterControllerCallbacks:(GCController*) controller
{
if (controller != NULL) {
controller.controllerPausedHandler = NULL;
if (controller.extendedGamepad != NULL) {
- // Re-enable system gestures on the gamepad buttons now
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- for (GCControllerElement* element in controller.physicalInputProfile.allElements) {
- element.preferredSystemGestureState = GCSystemGestureStateEnabled;
- }
- }
-
controller.extendedGamepad.valueChangedHandler = NULL;
}
- }
-}
-
--(void) initializeControllerHaptics:(Controller*) controller
-{
- controller.lowFreqMotor = [HapticContext createContextForLowFreqMotor:controller.gamepad];
- controller.highFreqMotor = [HapticContext createContextForHighFreqMotor:controller.gamepad];
- controller.leftTriggerMotor = [HapticContext createContextForLeftTrigger:controller.gamepad];
- controller.rightTriggerMotor = [HapticContext createContextForRightTrigger:controller.gamepad];
-}
-
--(void) cleanupControllerHaptics:(Controller*) controller
-{
- [controller.lowFreqMotor cleanup];
- [controller.highFreqMotor cleanup];
- [controller.leftTriggerMotor cleanup];
- [controller.rightTriggerMotor cleanup];
-}
-
--(void) cleanupControllerMotion:(Controller*) controller
-{
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- // Stop sensor sampling timers
- [controller.gyroTimer invalidate];
- [controller.accelTimer invalidate];
-
- // Disable motion sensors if they require manual activation
- if (controller.gamepad && controller.gamepad.motion && controller.gamepad.motion.sensorsRequireManualActivation) {
- controller.gamepad.motion.sensorsActive = NO;
+ else if (controller.gamepad != NULL) {
+ controller.gamepad.valueChangedHandler = NULL;
}
}
}
--(void) initializeControllerBattery:(Controller*) controller
-{
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- if (controller.gamepad.battery) {
- // Poll for updated battery status every 30 seconds
- controller.batteryTimer = [NSTimer scheduledTimerWithTimeInterval:30 repeats:YES block:^(NSTimer *timer) {
- if (controller.lastBatteryState != controller.gamepad.battery.batteryState ||
- controller.lastBatteryLevel != controller.gamepad.battery.batteryLevel) {
- uint8_t batteryState;
-
- switch (controller.gamepad.battery.batteryState) {
- case GCDeviceBatteryStateFull:
- batteryState = LI_BATTERY_STATE_FULL;
- break;
- case GCDeviceBatteryStateCharging:
- batteryState = LI_BATTERY_STATE_CHARGING;
- break;
- case GCDeviceBatteryStateDischarging:
- batteryState = LI_BATTERY_STATE_DISCHARGING;
- break;
- case GCDeviceBatteryStateUnknown:
- default:
- batteryState = LI_BATTERY_STATE_UNKNOWN;
- break;
- }
-
- LiSendControllerBatteryEvent(controller.playerIndex, batteryState, (uint8_t)(controller.gamepad.battery.batteryLevel * 100));
-
- controller.lastBatteryState = controller.gamepad.battery.batteryState;
- controller.lastBatteryLevel = controller.gamepad.battery.batteryLevel;
- }
- }];
-
- // Fire the timer immediately to send the initial battery state
- [controller.batteryTimer fire];
- }
- }
-}
-
--(void) cleanupControllerBattery:(Controller*) controller
-{
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- [controller.batteryTimer invalidate];
- }
-}
-
--(BOOL) reportControllerArrival:(Controller*) limeController
-{
- // Only report arrival once
- if (limeController.reportedArrival) {
- return YES;
- }
-
- uint8_t type = LI_CTYPE_UNKNOWN;
- uint16_t capabilities = 0;
- uint32_t supportedButtonFlags = 0;
-
- GCController *controller = limeController.gamepad;
- if (controller) {
- // This is a physical controller with a corresponding GCController object
-
- // Start is always present
- supportedButtonFlags |= PLAY_FLAG;
-
- // Detect buttons present in the GCExtendedGamepad profile
- if (controller.extendedGamepad.dpad) {
- supportedButtonFlags |= UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG;
- }
- if (controller.extendedGamepad.leftShoulder) {
- supportedButtonFlags |= LB_FLAG;
- }
- if (controller.extendedGamepad.rightShoulder) {
- supportedButtonFlags |= RB_FLAG;
- }
- if (@available(iOS 13.0, tvOS 13.0, *)) {
- if (controller.extendedGamepad.buttonOptions) {
- supportedButtonFlags |= BACK_FLAG;
- }
- }
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- if (controller.extendedGamepad.buttonHome) {
- supportedButtonFlags |= SPECIAL_FLAG;
- }
- }
- if (controller.extendedGamepad.buttonA) {
- supportedButtonFlags |= A_FLAG;
- }
- if (controller.extendedGamepad.buttonB) {
- supportedButtonFlags |= B_FLAG;
- }
- if (controller.extendedGamepad.buttonX) {
- supportedButtonFlags |= X_FLAG;
- }
- if (controller.extendedGamepad.buttonY) {
- supportedButtonFlags |= Y_FLAG;
- }
- if (@available(iOS 12.1, tvOS 12.1, *)) {
- if (controller.extendedGamepad.leftThumbstickButton) {
- supportedButtonFlags |= LS_CLK_FLAG;
- }
- if (controller.extendedGamepad.rightThumbstickButton) {
- supportedButtonFlags |= RS_CLK_FLAG;
- }
- }
-
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- // Xbox One/Series controller
- if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) {
- supportedButtonFlags |= PADDLE1_FLAG;
- }
- if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) {
- supportedButtonFlags |= PADDLE2_FLAG;
- }
- if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) {
- supportedButtonFlags |= PADDLE3_FLAG;
- }
- if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) {
- supportedButtonFlags |= PADDLE4_FLAG;
- }
- if (@available(iOS 15.0, tvOS 15.0, *)) {
- if (controller.physicalInputProfile.buttons[GCInputButtonShare]) {
- supportedButtonFlags |= MISC_FLAG;
- }
- }
-
- // DualShock/DualSense controller
- if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) {
- supportedButtonFlags |= TOUCHPAD_FLAG;
- }
- if (controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
- capabilities |= LI_CCAP_TOUCHPAD;
- }
-
- if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) {
- type = LI_CTYPE_XBOX;
- }
- else if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) {
- type = LI_CTYPE_PS;
- }
-
- if (@available(iOS 14.5, tvOS 14.5, *)) {
- if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
- type = LI_CTYPE_PS;
- }
- }
-
- // Detect supported haptics localities
- if (controller.haptics) {
- if ([controller.haptics.supportedLocalities containsObject:GCHapticsLocalityHandles]) {
- capabilities |= LI_CCAP_RUMBLE;
- }
- if ([controller.haptics.supportedLocalities containsObject:GCHapticsLocalityTriggers]) {
- capabilities |= LI_CCAP_TRIGGER_RUMBLE;
- }
- }
-
- // Detect supported motion sensors
- if (controller.motion) {
- if (controller.motion.hasGravityAndUserAcceleration) {
- capabilities |= LI_CCAP_ACCEL;
- }
- if (controller.motion.hasRotationRate) {
- capabilities |= LI_CCAP_GYRO;
- }
- }
-
- // Detect RGB LED support
- if (controller.light) {
- capabilities |= LI_CCAP_RGB_LED;
- }
-
- // Detect battery support
- if (controller.battery) {
- capabilities |= LI_CCAP_BATTERY_STATE;
- }
- }
- else {
- // This is a virtual controller corresponding to our OSC
-
- // TODO: Support various layouts and button labels on the OSC
- type = LI_CTYPE_XBOX;
- capabilities = 0;
- supportedButtonFlags =
- PLAY_FLAG | BACK_FLAG | UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG |
- LB_FLAG | RB_FLAG | LS_CLK_FLAG | RS_CLK_FLAG | A_FLAG | B_FLAG | X_FLAG | Y_FLAG;
- }
- }
-
- // Report the new controller to the host
- // NB: This will fail if the connection hasn't been fully established yet
- // and we will try again later.
- if (LiSendControllerArrivalEvent(controller.playerIndex,
- [self getActiveGamepadMask],
- type,
- supportedButtonFlags,
- capabilities) != 0) {
- return NO;
- }
-
- // Begin polling for battery status
- [self initializeControllerBattery:limeController];
-
- // Remember that we've reported arrival already
- limeController.reportedArrival = YES;
- return YES;
-}
-
--(void) handleControllerTouchpad:(Controller*)controller touch:(GCControllerDirectionPad*)touch index:(int)index
-{
- controller_touch_context_t context = index == 0 ? controller.primaryTouch : controller.secondaryTouch;
-
- // This magic is courtesy of SDL
- float normalizedX = (1.0f + touch.xAxis.value) * 0.5f;
- float normalizedY = 1.0f - (1.0f + touch.yAxis.value) * 0.5f;
-
- // If we went from a touch to no touch, generate a touch up event
- if ((context.lastX || context.lastY) && (!touch.xAxis.value && !touch.yAxis.value)) {
- LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_UP, index, normalizedX, normalizedY, 1.0f);
- }
- else if (touch.xAxis.value || touch.yAxis.value) {
- // If we went from no touch to a touch, generate a touch down event
- if (!context.lastX && !context.lastY) {
- LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_DOWN, index, normalizedX, normalizedY, 1.0f);
- }
- else if (context.lastX != touch.xAxis.value || context.lastY != touch.yAxis.value) {
- // Otherwise it's just a move
- LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_MOVE, index, normalizedX, normalizedY, 1.0f);
- }
- }
-
- // We have to assign the whole struct because this is a property rather than a standard
- // field that we could modify through a pointer.
- if (index == 0) {
- controller.primaryTouch = (controller_touch_context_t) {
- touch.xAxis.value,
- touch.yAxis.value
- };
- }
- else {
- controller.secondaryTouch = (controller_touch_context_t) {
- touch.xAxis.value,
- touch.yAxis.value
- };
- }
-}
-
-(void) registerControllerCallbacks:(GCController*) controller
{
if (controller != NULL) {
- // iOS 13 allows the Start button to behave like a normal button, however
- // older MFi controllers can send an instant down+up event for the start button
- // which means the button will not be down long enough to register on the PC.
- // To work around this issue, use the old controllerPausedHandler if the controller
- // doesn't have a Select button (which indicates it probably doesn't have a proper
- // Start button either).
- BOOL useLegacyPausedHandler = YES;
- if (@available(iOS 13.0, tvOS 13.0, *)) {
- if (controller.extendedGamepad != nil &&
- controller.extendedGamepad.buttonOptions != nil) {
- useLegacyPausedHandler = NO;
- }
- }
-
- if (useLegacyPausedHandler) {
- controller.controllerPausedHandler = ^(GCController *controller) {
- Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
-
- // Get off the main thread
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- [self setButtonFlag:limeController flags:PLAY_FLAG];
- [self updateFinished:limeController];
-
- // Pause for 100 ms
- usleep(100 * 1000);
-
- [self clearButtonFlag:limeController flags:PLAY_FLAG];
- [self updateFinished:limeController];
- });
- };
- }
+ controller.controllerPausedHandler = ^(GCController *controller) {
+ Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
+ [self setButtonFlag:limeController flags:PLAY_FLAG];
+ [self updateFinished:limeController];
+
+ // Pause for 100 ms
+ usleep(100 * 1000);
+
+ [self clearButtonFlag:limeController flags:PLAY_FLAG];
+ [self updateFinished:limeController];
+ };
if (controller.extendedGamepad != NULL) {
- // Disable system gestures on the gamepad to avoid interfering
- // with in-game controller actions
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- for (GCControllerElement* element in controller.physicalInputProfile.allElements) {
- element.preferredSystemGestureState = GCSystemGestureStateDisabled;
- }
- }
-
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
- Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
+ Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
- unsigned char leftTrigger, rightTrigger;
+ char leftTrigger, rightTrigger;
- if (self->_swapABXYButtons) {
- UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonA.pressed);
- UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonB.pressed);
- UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonX.pressed);
- UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonY.pressed);
- }
- else {
- UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
- UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
- UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
- UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonY.pressed);
- }
+ UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
+ UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
+ UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
+ UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonY.pressed);
UPDATE_BUTTON_FLAG(limeController, UP_FLAG, gamepad.dpad.up.pressed);
UPDATE_BUTTON_FLAG(limeController, DOWN_FLAG, gamepad.dpad.down.pressed);
@@ -747,68 +200,6 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
- // Yay, iOS 12.1 now supports analog stick buttons
- if (@available(iOS 12.1, tvOS 12.1, *)) {
- if (gamepad.leftThumbstickButton != nil) {
- UPDATE_BUTTON_FLAG(limeController, LS_CLK_FLAG, gamepad.leftThumbstickButton.pressed);
- }
- if (gamepad.rightThumbstickButton != nil) {
- UPDATE_BUTTON_FLAG(limeController, RS_CLK_FLAG, gamepad.rightThumbstickButton.pressed);
- }
- }
-
- if (@available(iOS 13.0, tvOS 13.0, *)) {
- // Options button is optional (only present on Xbox One S and PS4 gamepads)
- if (gamepad.buttonOptions != nil) {
- UPDATE_BUTTON_FLAG(limeController, BACK_FLAG, gamepad.buttonOptions.pressed);
-
- // For older MFi gamepads, the menu button will already be handled by
- // the controllerPausedHandler.
- UPDATE_BUTTON_FLAG(limeController, PLAY_FLAG, gamepad.buttonMenu.pressed);
- }
- }
-
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- // Home/Guide button is optional (only present on Xbox One S and PS4 gamepads)
- if (gamepad.buttonHome != nil) {
- UPDATE_BUTTON_FLAG(limeController, SPECIAL_FLAG, gamepad.buttonHome.pressed);
- }
-
- // Xbox One/Series controllers
- if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) {
- UPDATE_BUTTON_FLAG(limeController, PADDLE1_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].pressed);
- }
- if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) {
- UPDATE_BUTTON_FLAG(limeController, PADDLE2_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].pressed);
- }
- if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) {
- UPDATE_BUTTON_FLAG(limeController, PADDLE3_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].pressed);
- }
- if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) {
- UPDATE_BUTTON_FLAG(limeController, PADDLE4_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].pressed);
- }
- if (@available(iOS 15.0, tvOS 15.0, *)) {
- if (gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare]) {
- UPDATE_BUTTON_FLAG(limeController, MISC_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare].pressed);
- }
- }
-
- // DualShock/DualSense controllers
- if (gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) {
- UPDATE_BUTTON_FLAG(limeController, TOUCHPAD_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].pressed);
- }
- if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
- [self handleControllerTouchpad:limeController
- touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]
- index:0];
- }
- if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
- [self handleControllerTouchpad:limeController
- touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
- index:1];
- }
- }
-
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
@@ -824,98 +215,30 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
[self updateFinished:limeController];
};
}
+ else if (controller.gamepad != NULL) {
+ controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, GCControllerElement *element) {
+ Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
+ UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
+ UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
+ UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
+ UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonY.pressed);
+
+ UPDATE_BUTTON_FLAG(limeController, UP_FLAG, gamepad.dpad.up.pressed);
+ UPDATE_BUTTON_FLAG(limeController, DOWN_FLAG, gamepad.dpad.down.pressed);
+ UPDATE_BUTTON_FLAG(limeController, LEFT_FLAG, gamepad.dpad.left.pressed);
+ UPDATE_BUTTON_FLAG(limeController, RIGHT_FLAG, gamepad.dpad.right.pressed);
+
+ UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
+ UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
+
+ [self updateFinished:limeController];
+ };
+ }
} else {
Log(LOG_W, @"Tried to register controller callbacks on NULL controller");
}
}
--(void) unregisterMouseCallbacks:(GCMouse*)mouse API_AVAILABLE(ios(14.0)) {
- mouse.mouseInput.mouseMovedHandler = nil;
-
- mouse.mouseInput.leftButton.pressedChangedHandler = nil;
- mouse.mouseInput.middleButton.pressedChangedHandler = nil;
- mouse.mouseInput.rightButton.pressedChangedHandler = nil;
-
- for (GCControllerButtonInput* auxButton in mouse.mouseInput.auxiliaryButtons) {
- auxButton.pressedChangedHandler = nil;
- }
-
-#if TARGET_OS_TV
- mouse.mouseInput.scroll.xAxis.valueChangedHandler = nil;
- mouse.mouseInput.scroll.yAxis.valueChangedHandler = nil;
-#endif
-}
-
--(void) registerMouseCallbacks:(GCMouse*) mouse API_AVAILABLE(ios(14.0)) {
- mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput * _Nonnull mouse, float deltaX, float deltaY) {
- self->accumulatedDeltaX += deltaX / MOUSE_SPEED_DIVISOR;
- self->accumulatedDeltaY += -deltaY / MOUSE_SPEED_DIVISOR;
-
- short truncatedDeltaX = (short)self->accumulatedDeltaX;
- short truncatedDeltaY = (short)self->accumulatedDeltaY;
-
- if (truncatedDeltaX != 0 || truncatedDeltaY != 0) {
- LiSendMouseMoveEvent(truncatedDeltaX, truncatedDeltaY);
-
- self->accumulatedDeltaX -= truncatedDeltaX;
- self->accumulatedDeltaY -= truncatedDeltaY;
- }
- };
-
- mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
- LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT);
- };
- mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
- LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
- };
- mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
- LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
- };
-
- if (mouse.mouseInput.auxiliaryButtons != nil) {
- if (mouse.mouseInput.auxiliaryButtons.count >= 1) {
- mouse.mouseInput.auxiliaryButtons[0].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
- LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X1);
- };
- }
- if (mouse.mouseInput.auxiliaryButtons.count >= 2) {
- mouse.mouseInput.auxiliaryButtons[1].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
- LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X2);
- };
- }
- }
-
- // We use UIPanGestureRecognizer on iPadOS because it allows us to distinguish
- // between discrete and continuous scroll events and also works around a bug
- // in iPadOS 15 where discrete scroll events are dropped. tvOS only supports
- // GCMouse for mice, so we will have to just use it and hope for the best.
-#if TARGET_OS_TV
- mouse.mouseInput.scroll.xAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) {
- self->accumulatedScrollX += value;
-
- short truncatedScrollX = (short)self->accumulatedScrollX;
-
- if (truncatedScrollX != 0) {
- // Direction is reversed from vertical scrolling
- LiSendHighResHScrollEvent(-truncatedScrollX * 20);
-
- self->accumulatedScrollX -= truncatedScrollX;
- }
- };
- mouse.mouseInput.scroll.yAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) {
- self->accumulatedScrollY += value;
-
- short truncatedScrollY = (short)self->accumulatedScrollY;
-
- if (truncatedScrollY != 0) {
- LiSendHighResScrollEvent(truncatedScrollY * 20);
-
- self->accumulatedScrollY -= truncatedScrollY;
- }
- };
-#endif
-}
-
-(void) updateAutoOnScreenControlMode
{
// Auto on-screen control support may not be enabled
@@ -933,34 +256,15 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
if (controller != NULL) {
if (controller.extendedGamepad != NULL) {
level = OnScreenControlsLevelAutoGCExtendedGamepad;
- if (@available(iOS 12.1, tvOS 12.1, *)) {
- if (controller.extendedGamepad.leftThumbstickButton != nil &&
- controller.extendedGamepad.rightThumbstickButton != nil) {
- level = OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons;
- if (@available(iOS 13.0, tvOS 13.0, *)) {
- if (controller.extendedGamepad.buttonOptions != nil) {
- // Has L3/R3 and Select, so we can show nothing :)
- level = OnScreenControlsLevelOff;
- }
- }
- }
- }
+ break;
+ }
+ else if (controller.gamepad != NULL) {
+ level = OnScreenControlsLevelAutoGCGamepad;
break;
}
}
}
- // If we didn't find a gamepad present and we have a keyboard or mouse, turn
- // the on-screen controls off to get the overlays out of the way.
- if (level == OnScreenControlsLevelFull && [ControllerSupport hasKeyboardOrMouse]) {
- level = OnScreenControlsLevelOff;
-
- // Ensure the virtual gamepad disappears to avoid confusing some games.
- // If the mouse and keyboard disconnect later, it will reappear when the
- // first OSC input is received.
- LiSendMultiControllerEvent(0, 0, 0, 0, 0, 0, 0, 0, 0);
- }
-
[_osc setLevel:level];
}
@@ -971,301 +275,86 @@ static const double MOUSE_SPEED_DIVISOR = 1.25;
[self updateAutoOnScreenControlMode];
}
--(Controller*) assignController:(GCController*)controller {
+-(void) assignController:(GCController*)controller {
for (int i = 0; i < 4; i++) {
if (!(_controllerNumbers & (1 << i))) {
_controllerNumbers |= (1 << i);
controller.playerIndex = i;
- Controller* limeController = [[Controller alloc] init];
- limeController.playerIndex = i;
- limeController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT;
- limeController.gamepad = controller;
-
- // If this is player 0, it shares state with the OSC
- limeController.mergedWithController = _oscController;
- _oscController.mergedWithController = limeController;
-
- if (@available(iOS 13.0, tvOS 13.0, *)) {
- if (controller.extendedGamepad != nil &&
- controller.extendedGamepad.buttonOptions != nil) {
- // Disable select button emulation since we have a physical select button
- limeController.supportedEmulationFlags &= ~EMULATING_SELECT;
- }
+ Controller* limeController;
+ if (i == 0) {
+ // Player 0 shares a controller object with the on-screen controls
+ limeController = _player0osc;
+ } else {
+ limeController = [[Controller alloc] init];
+ limeController.playerIndex = i;
}
-
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- if (controller.extendedGamepad != nil &&
- controller.extendedGamepad.buttonHome != nil) {
- // Disable special button emulation since we have a physical special button
- limeController.supportedEmulationFlags &= ~EMULATING_SPECIAL;
- }
- }
-
- // Prepare controller haptics for use
- [self initializeControllerHaptics:limeController];
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
Log(LOG_I, @"Assigning controller index: %d", i);
- return limeController;
+ break;
}
}
-
- return nil;
}
-(Controller*) getOscController {
- return _oscController;
+ return _player0osc;
}
-+(bool) isSupportedGamepad:(GCController*) controller {
- return controller.extendedGamepad != nil;
-}
-
-#pragma clang diagnostic pop
-
-+(int) getGamepadCount {
- int count = 0;
-
- for (GCController* controller in [GCController controllers]) {
- if ([ControllerSupport isSupportedGamepad:controller]) {
- count++;
- }
- }
-
- return count;
-}
-
-+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig {
- int mask = 0;
-
- if (streamConfig.multiController) {
- int i = 0;
- for (GCController* controller in [GCController controllers]) {
- if ([ControllerSupport isSupportedGamepad:controller]) {
- mask |= 1 << i++;
- }
- }
- }
- else {
- // Some games don't deal with having controller reconnected
- // properly so always report controller 1 if not in MC mode
- mask = 0x1;
- }
-
- DataManager* dataMan = [[DataManager alloc] init];
- TemporarySettings* settings = [dataMan getSettings];
- OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
-
- // Even if no gamepads are present, we will always count one if OSC is enabled,
- // or it's set to auto and no keyboard or mouse is present. Absolute touch mode
- // disables the OSC.
- if (level != OnScreenControlsLevelOff && (![ControllerSupport hasKeyboardOrMouse] || level != OnScreenControlsLevelAuto) && !settings.absoluteTouchMode) {
- mask |= 0x1;
- }
-
- return mask;
-}
-
--(NSUInteger) getConnectedGamepadCount
-{
- return _controllers.count;
-}
-
--(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id)delegate
+-(id) init
{
self = [super init];
_controllerStreamLock = [[NSLock alloc] init];
_controllers = [[NSMutableDictionary alloc] init];
_controllerNumbers = 0;
- _multiController = streamConfig.multiController;
- _swapABXYButtons = streamConfig.swapABXYButtons;
- _delegate = delegate;
-
- _oscController = [[Controller alloc] init];
- _oscController.playerIndex = 0;
-
- DataManager* dataMan = [[DataManager alloc] init];
- _oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
-
- Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]);
- Log(LOG_I, @"Multi-controller: %d", _multiController);
+ _player0osc = [[Controller alloc] init];
+ _player0osc.playerIndex = 0;
+ Log(LOG_I, @"Number of controllers connected: %ld", (long)[[GCController controllers] count]);
for (GCController* controller in [GCController controllers]) {
- if ([ControllerSupport isSupportedGamepad:controller]) {
- [self assignController:controller];
- [self registerControllerCallbacks:controller];
-
- // Note: We cannot report controller arrival to the host here,
- // because the connection has not been established yet.
- }
+ [self assignController:controller];
+ [self registerControllerCallbacks:controller];
+ [self updateAutoOnScreenControlMode];
}
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- for (GCMouse* mouse in [GCMouse mice]) {
- [self registerMouseCallbacks:mouse];
- }
- }
-
- _controllerConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Controller connected!");
GCController* controller = note.object;
+ [self assignController:controller];
- if (![ControllerSupport isSupportedGamepad:controller]) {
- // Ignore micro gamepads and motion controllers
- return;
- }
+ // Register callbacks on the new controller
+ [self registerControllerCallbacks:controller];
- Controller* limeController = [self assignController:controller];
- if (limeController) {
- // Register callbacks on the new controller
- [self registerControllerCallbacks:controller];
-
- // Report the controller arrival to the host if we're connected
- [self reportControllerArrival:limeController];
-
- // Re-evaluate the on-screen control mode
- [self updateAutoOnScreenControlMode];
-
- // Notify the delegate
- [self->_delegate gamepadPresenceChanged];
- }
+ // Re-evaluate the on-screen control mode
+ [self updateAutoOnScreenControlMode];
}];
- _controllerDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ self.disconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Controller disconnected!");
GCController* controller = note.object;
-
- if (![ControllerSupport isSupportedGamepad:controller]) {
- // Ignore micro gamepads and motion controllers
- return;
- }
-
[self unregisterControllerCallbacks:controller];
- self->_controllerNumbers &= ~(1 << controller.playerIndex);
+ [_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
+ _controllerNumbers &= ~(1 << controller.playerIndex);
Log(LOG_I, @"Unassigning controller index: %ld", (long)controller.playerIndex);
- Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
- if (limeController) {
- // Stop haptics on this controller
- [self cleanupControllerHaptics:limeController];
-
- // Stop motion reports on this controller
- [self cleanupControllerMotion:limeController];
-
- // Stop battery reports on this controller
- [self cleanupControllerBattery:limeController];
-
- // Disassociate this controller from any controllers merged with it
- if (limeController.mergedWithController) {
- assert(limeController.mergedWithController.mergedWithController == limeController);
- limeController.mergedWithController.mergedWithController = nil;
- }
-
- // Inform the server of the updated active gamepads before removing this controller
- [self updateFinished:limeController];
- [self->_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
-
- // Re-evaluate the on-screen control mode
- [self updateAutoOnScreenControlMode];
-
- // Notify the delegate
- [self->_delegate gamepadPresenceChanged];
- }
+ // Re-evaluate the on-screen control mode
+ [self updateAutoOnScreenControlMode];
}];
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- _mouseConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Log(LOG_I, @"Mouse connected!");
-
- GCMouse* mouse = note.object;
-
- // Register for mouse events
- [self registerMouseCallbacks: mouse];
-
- // Re-evaluate the on-screen control mode
- [self updateAutoOnScreenControlMode];
-
- // Notify the delegate
- [self->_delegate mousePresenceChanged];
- }];
- _mouseDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Log(LOG_I, @"Mouse disconnected!");
-
- GCMouse* mouse = note.object;
-
- // Unregister for mouse events
- [self unregisterMouseCallbacks: mouse];
-
- // Re-evaluate the on-screen control mode
- [self updateAutoOnScreenControlMode];
-
- // Notify the delegate
- [self->_delegate mousePresenceChanged];
- }];
- _keyboardConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Log(LOG_I, @"Keyboard connected!");
-
- // Re-evaluate the on-screen control mode
- [self updateAutoOnScreenControlMode];
- }];
- _keyboardDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Log(LOG_I, @"Keyboard disconnected!");
-
- // Re-evaluate the on-screen control mode
- [self updateAutoOnScreenControlMode];
- }];
- }
-
return self;
}
--(void) connectionEstablished
-{
- for (Controller* controller in [_controllers allValues]) {
- // Report the controller arrival to the host if we haven't done so yet
- [self reportControllerArrival:controller];
- }
-}
-
-(void) cleanup
{
- [[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver];
- [[NSNotificationCenter defaultCenter] removeObserver:_controllerDisconnectObserver];
- [[NSNotificationCenter defaultCenter] removeObserver:_mouseConnectObserver];
- [[NSNotificationCenter defaultCenter] removeObserver:_mouseDisconnectObserver];
- [[NSNotificationCenter defaultCenter] removeObserver:_keyboardConnectObserver];
- [[NSNotificationCenter defaultCenter] removeObserver:_keyboardDisconnectObserver];
-
- _controllerConnectObserver = nil;
- _controllerDisconnectObserver = nil;
- _mouseConnectObserver = nil;
- _mouseDisconnectObserver = nil;
- _keyboardConnectObserver = nil;
- _keyboardDisconnectObserver = nil;
-
- _controllerNumbers = 0;
-
- for (Controller* controller in [_controllers allValues]) {
- [self cleanupControllerHaptics:controller];
- [self cleanupControllerMotion:controller];
- [self cleanupControllerBattery:controller];
- }
+ [[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
+ [[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
[_controllers removeAllObjects];
-
+ _controllerNumbers = 0;
for (GCController* controller in [GCController controllers]) {
- if ([ControllerSupport isSupportedGamepad:controller]) {
- [self unregisterControllerCallbacks:controller];
- }
- }
-
- if (@available(iOS 14.0, tvOS 14.0, *)) {
- for (GCMouse* mouse in [GCMouse mice]) {
- [self unregisterMouseCallbacks:mouse];
- }
+ [self unregisterControllerCallbacks:controller];
}
}
diff --git a/Limelight/Input/HapticContext.h b/Limelight/Input/HapticContext.h
deleted file mode 100644
index b8f2b61..0000000
--- a/Limelight/Input/HapticContext.h
+++ /dev/null
@@ -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
diff --git a/Limelight/Input/HapticContext.m b/Limelight/Input/HapticContext.m
deleted file mode 100644
index b61124c..0000000
--- a/Limelight/Input/HapticContext.m
+++ /dev/null
@@ -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 _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
diff --git a/Limelight/Input/KeyboardInputField.h b/Limelight/Input/KeyboardInputField.h
deleted file mode 100644
index eff86eb..0000000
--- a/Limelight/Input/KeyboardInputField.h
+++ /dev/null
@@ -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
-
-NS_ASSUME_NONNULL_BEGIN
-
-@interface KeyboardInputField : UITextField
-
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/Limelight/Input/KeyboardInputField.m b/Limelight/Input/KeyboardInputField.m
deleted file mode 100644
index 2fb0e73..0000000
--- a/Limelight/Input/KeyboardInputField.m
+++ /dev/null
@@ -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
diff --git a/Limelight/Input/KeyboardSupport.h b/Limelight/Input/KeyboardSupport.h
deleted file mode 100644
index 6cecb4d..0000000
--- a/Limelight/Input/KeyboardSupport.h
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// KeyboardSupport.h
-// Moonlight
-//
-// Created by Diego Waxemberg on 8/25/18.
-// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
-//
-
-#import
-
-@interface KeyboardSupport : NSObject
-
-struct KeyEvent {
- u_short keycode;
- u_short modifierKeycode;
- u_char modifier;
-};
-
-+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4));
-+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
-+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
-
-@end
diff --git a/Limelight/Input/KeyboardSupport.m b/Limelight/Input/KeyboardSupport.m
deleted file mode 100644
index f4b8f5a..0000000
--- a/Limelight/Input/KeyboardSupport.m
+++ /dev/null
@@ -1,452 +0,0 @@
-//
-// KeyboardSupport.m
-// Moonlight
-//
-// Created by Diego Waxemberg on 8/25/18.
-// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
-//
-
-#import "KeyboardSupport.h"
-#include
-
-@implementation KeyboardSupport
-
-+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4)) {
- if (press.key != nil) {
- return [KeyboardSupport sendKeyEvent:press.key down:down];
- }
- else {
- short keyCode;
-
- switch (press.type) {
- case UIPressTypeUpArrow:
- keyCode = 0x26;
- break;
- case UIPressTypeDownArrow:
- keyCode = 0x28;
- break;
- case UIPressTypeLeftArrow:
- keyCode = 0x25;
- break;
- case UIPressTypeRightArrow:
- keyCode = 0x27;
- break;
- default:
- // Unhandled press type
- return NO;
- }
-
- LiSendKeyboardEvent(0x8000 | keyCode,
- down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
- 0);
-
- return YES;
- }
-}
-
-+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)) {
- char modifierFlags = 0;
- short keyCode = 0;
-
- if (key.modifierFlags & UIKeyModifierShift) {
- modifierFlags |= MODIFIER_SHIFT;
- }
- if (key.modifierFlags & UIKeyModifierAlternate) {
- modifierFlags |= MODIFIER_ALT;
- }
- if (key.modifierFlags & UIKeyModifierControl) {
- modifierFlags |= MODIFIER_CTRL;
- }
- if (key.modifierFlags & UIKeyModifierCommand) {
- modifierFlags |= MODIFIER_META;
- }
-
- // This converts UIKeyboardHIDUsage values to Win32 VK_* values
- // https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
- if (key.keyCode >= UIKeyboardHIDUsageKeyboardA &&
- key.keyCode <= UIKeyboardHIDUsageKeyboardZ) {
- keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardA) + 0x41;
- }
- else if (key.keyCode == UIKeyboardHIDUsageKeyboard0) {
- // This key is at the beginning of the VK_ range but the end
- // of the UIKeyboardHIDUsageKeyboard range.
- keyCode = 0x30;
- }
- else if (key.keyCode >= UIKeyboardHIDUsageKeyboard1 &&
- key.keyCode <= UIKeyboardHIDUsageKeyboard9) {
- keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboard1) + 0x31;
- }
- else if (key.keyCode == UIKeyboardHIDUsageKeypad0) {
- // This key is at the beginning of the VK_ range but the end
- // of the UIKeyboardHIDUsageKeypad range.
- keyCode = 0x60;
- }
- else if (key.keyCode >= UIKeyboardHIDUsageKeypad1 &&
- key.keyCode <= UIKeyboardHIDUsageKeypad9) {
- keyCode = (key.keyCode - UIKeyboardHIDUsageKeypad1) + 0x61;
- }
- else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF1 &&
- key.keyCode <= UIKeyboardHIDUsageKeyboardF12) {
- keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF1) + 0x70;
- }
- else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF13 &&
- key.keyCode <= UIKeyboardHIDUsageKeyboardF24) {
- keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF13) + 0x7C;
- }
- else {
- switch (key.keyCode) {
- case UIKeyboardHIDUsageKeyboardReturnOrEnter:
- keyCode = 0x0D;
- break;
- case UIKeyboardHIDUsageKeyboardEscape:
- keyCode = 0x1B;
- break;
- case UIKeyboardHIDUsageKeyboardDeleteOrBackspace:
- keyCode = 0x08;
- break;
- case UIKeyboardHIDUsageKeyboardTab:
- keyCode = 0x09;
- break;
- case UIKeyboardHIDUsageKeyboardSpacebar:
- keyCode = 0x20;
- break;
- case UIKeyboardHIDUsageKeyboardHyphen:
- keyCode = 0xBD;
- break;
- case UIKeyboardHIDUsageKeyboardEqualSign:
- keyCode = 0xBB;
- break;
- case UIKeyboardHIDUsageKeyboardOpenBracket:
- keyCode = 0xDB;
- break;
- case UIKeyboardHIDUsageKeyboardCloseBracket:
- keyCode = 0xDD;
- break;
- case UIKeyboardHIDUsageKeyboardBackslash:
- keyCode = 0xDC;
- break;
- case UIKeyboardHIDUsageKeyboardSemicolon:
- keyCode = 0xBA;
- break;
- case UIKeyboardHIDUsageKeyboardQuote:
- keyCode = 0xDE;
- break;
- case UIKeyboardHIDUsageKeyboardGraveAccentAndTilde:
- keyCode = 0xC0;
- break;
- case UIKeyboardHIDUsageKeyboardComma:
- keyCode = 0xBC;
- break;
- case UIKeyboardHIDUsageKeyboardPeriod:
- keyCode = 0xBE;
- break;
- case UIKeyboardHIDUsageKeyboardSlash:
- keyCode = 0xBF;
- break;
- case UIKeyboardHIDUsageKeyboardCapsLock:
- keyCode = 0x14;
- break;
- case UIKeyboardHIDUsageKeyboardPrintScreen:
- keyCode = 0x2A;
- break;
- case UIKeyboardHIDUsageKeyboardScrollLock:
- keyCode = 0x91;
- break;
- case UIKeyboardHIDUsageKeyboardPause:
- keyCode = 0x13;
- break;
- case UIKeyboardHIDUsageKeyboardInsert:
- keyCode = 0x2D;
- break;
- case UIKeyboardHIDUsageKeyboardHome:
- keyCode = 0x24;
- break;
- case UIKeyboardHIDUsageKeyboardPageUp:
- keyCode = 0x21;
- break;
- case UIKeyboardHIDUsageKeyboardDeleteForward:
- keyCode = 0x2E;
- break;
- case UIKeyboardHIDUsageKeyboardEnd:
- keyCode = 0x23;
- break;
- case UIKeyboardHIDUsageKeyboardPageDown:
- keyCode = 0x22;
- break;
- case UIKeyboardHIDUsageKeyboardRightArrow:
- keyCode = 0x27;
- break;
- case UIKeyboardHIDUsageKeyboardLeftArrow:
- keyCode = 0x25;
- break;
- case UIKeyboardHIDUsageKeyboardDownArrow:
- keyCode = 0x28;
- break;
- case UIKeyboardHIDUsageKeyboardUpArrow:
- keyCode = 0x26;
- break;
- case UIKeyboardHIDUsageKeypadNumLock:
- keyCode = 0x90;
- break;
- case UIKeyboardHIDUsageKeypadSlash:
- keyCode = 0x6F;
- break;
- case UIKeyboardHIDUsageKeypadAsterisk:
- keyCode = 0x6A;
- break;
- case UIKeyboardHIDUsageKeypadHyphen:
- keyCode = 0x6D;
- break;
- case UIKeyboardHIDUsageKeypadPlus:
- keyCode = 0x6B;
- break;
- case UIKeyboardHIDUsageKeypadEnter:
- keyCode = 0x0D;
- modifierFlags |= MODIFIER_EXTENDED;
- break;
- case UIKeyboardHIDUsageKeypadPeriod:
- keyCode = 0x6E;
- break;
- case UIKeyboardHIDUsageKeyboardNonUSBackslash:
- keyCode = 0xE2;
- break;
- case UIKeyboardHIDUsageKeypadComma:
- keyCode = 0x6C;
- break;
- case UIKeyboardHIDUsageKeyboardCancel:
- keyCode = 0x03;
- break;
- case UIKeyboardHIDUsageKeyboardClear:
- keyCode = 0x0C;
- break;
- case UIKeyboardHIDUsageKeyboardCrSelOrProps:
- keyCode = 0xF7;
- break;
- case UIKeyboardHIDUsageKeyboardExSel:
- keyCode = 0xF8;
- break;
- case UIKeyboardHIDUsageKeyboardLeftGUI:
- keyCode = 0x5B;
- break;
- case UIKeyboardHIDUsageKeyboardLeftControl:
- keyCode = 0xA2;
- break;
- case UIKeyboardHIDUsageKeyboardLeftShift:
- keyCode = 0xA0;
- break;
- case UIKeyboardHIDUsageKeyboardLeftAlt:
- keyCode = 0xA4;
- break;
- case UIKeyboardHIDUsageKeyboardRightGUI:
- keyCode = 0x5C;
- break;
- case UIKeyboardHIDUsageKeyboardRightControl:
- keyCode = 0xA3;
- break;
- case UIKeyboardHIDUsageKeyboardRightShift:
- keyCode = 0xA1;
- break;
- case UIKeyboardHIDUsageKeyboardRightAlt:
- keyCode = 0xA5;
- break;
- case 669: // This value corresponds to the "Globe" or "Language" key on most Apple branded iPad keyboards.
- keyCode = 0x1B; // This value corresponds to "Escape", which is missing from most Apple branded iPad keyboards.
- break;
- default:
- NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
- assert(0);
- return false;
- }
- }
-
- LiSendKeyboardEvent(0x8000 | keyCode,
- down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
- modifierFlags);
- return true;
-}
-
-+ (struct KeyEvent)translateKeyEvent:(unichar)inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags {
- struct KeyEvent event;
- event.keycode = 0;
- event.modifier = 0;
- event.modifierKeycode = 0;
-
- switch (modifierFlags) {
- case UIKeyModifierAlphaShift:
- case UIKeyModifierShift:
- [KeyboardSupport addShiftModifier:&event];
- break;
- case UIKeyModifierControl:
- [KeyboardSupport addControlModifier:&event];
- break;
- case UIKeyModifierCommand:
- [KeyboardSupport addMetaModifier:&event];
- break;
- case UIKeyModifierAlternate:
- [KeyboardSupport addAltModifier:&event];
- break;
- case UIKeyModifierNumericPad:
- break;
- }
- if (inputChar >= 0x30 && inputChar <= 0x39) {
- // Numbers 0-9
- event.keycode = inputChar;
- } else if (inputChar >= 0x41 && inputChar <= 0x5A) {
- // Capital letters
- event.keycode = inputChar;
- [KeyboardSupport addShiftModifier:&event];
- } else if (inputChar >= 0x61 && inputChar <= 0x7A) {
- // Lower case letters
- event.keycode = inputChar - (0x61 - 0x41);
- } switch (inputChar) {
- case ' ': // Spacebar
- event.keycode = 0x20;
- break;
- case '-': // Hyphen '-'
- event.keycode = 0xBD;
- break;
- case '/': // Forward slash '/'
- event.keycode = 0xBF;
- break;
- case ':': // Colon ':'
- event.keycode = 0xBA;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case ';': // Semi-colon ';'
- event.keycode = 0xBA;
- break;
- case '(': // Open parenthesis '('
- event.keycode = 0x39; // '9'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case ')': // Close parenthesis ')'
- event.keycode = 0x30; // '0'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '$': // Dollar sign '$'
- event.keycode = 0x34; // '4'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '&': // Ampresand '&'
- event.keycode = 0x37; // '7'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '@': // At-sign '@'
- event.keycode = 0x32; // '2'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '"':
- event.keycode = 0xDE;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '\'':
- event.keycode = 0xDE;
- break;
- case '!':
- event.keycode = 0x31; // '1'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '?':
- event.keycode = 0xBF; // '/'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case ',':
- event.keycode = 0xBC;
- break;
- case '<':
- event.keycode = 0xBC;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '.':
- event.keycode = 0xBE;
- break;
- case '>':
- event.keycode = 0xBE;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '[':
- event.keycode = 0xDB;
- break;
- case ']':
- event.keycode = 0xDD;
- break;
- case '{':
- event.keycode = 0xDB;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '}':
- event.keycode = 0xDD;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '#':
- event.keycode = 0x33; // '3'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '%':
- event.keycode = 0x35; // '5'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '^':
- event.keycode = 0x36; // '6'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '*':
- event.keycode = 0x38; // '8'
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '+':
- event.keycode = 0xBB;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '=':
- event.keycode = 0xBB;
- break;
- case '_':
- event.keycode = 0xBD;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '\\':
- event.keycode = 0xDC;
- break;
- case '|':
- event.keycode = 0xDC;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '~':
- event.keycode = 0xC0;
- [KeyboardSupport addShiftModifier:&event];
- break;
- case '`':
- event.keycode = 0xC0;
- break;
- case '\t':
- event.keycode = 0x09;
- break;
- default:
- break;
- }
-
- return event;
-}
-
-+ (void) addShiftModifier:(struct KeyEvent*)event {
- event->modifier = MODIFIER_SHIFT;
- event->modifierKeycode = 0x10;
-}
-
-+ (void) addControlModifier:(struct KeyEvent*)event {
- event->modifier = MODIFIER_CTRL;
- event->modifierKeycode = 0x11;
-}
-
-+ (void) addMetaModifier:(struct KeyEvent*)event {
- event->modifier = MODIFIER_META;
- event->modifierKeycode = 0x5B;
-}
-
-+ (void) addAltModifier:(struct KeyEvent*)event {
- event->modifier = MODIFIER_ALT;
- event->modifierKeycode = 0x12;
-}
-
-@end
diff --git a/Limelight/Input/OnScreenControls.h b/Limelight/Input/OnScreenControls.h
index 29997de..23ee57b 100644
--- a/Limelight/Input/OnScreenControls.h
+++ b/Limelight/Input/OnScreenControls.h
@@ -7,9 +7,9 @@
//
#import
+#import "StreamView.h"
@class ControllerSupport;
-@class StreamConfiguration;
@interface OnScreenControls : NSObject
@@ -22,15 +22,12 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
// Internal levels selected by ControllerSupport
OnScreenControlsLevelAutoGCGamepad,
OnScreenControlsLevelAutoGCExtendedGamepad,
- OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
};
-- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig;
+- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id)edgeDelegate;
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
- (void) setLevel:(OnScreenControlsLevel)level;
-- (OnScreenControlsLevel) getLevel;
-- (void) show;
@end
diff --git a/Limelight/Input/OnScreenControls.m b/Limelight/Input/OnScreenControls.m
index 846492a..c02729f 100644
--- a/Limelight/Input/OnScreenControls.m
+++ b/Limelight/Input/OnScreenControls.m
@@ -7,11 +7,13 @@
//
#import "OnScreenControls.h"
-#import "StreamView.h"
#import "ControllerSupport.h"
-#import "Controller.h"
+//#import "Controller.h"
#include "Limelight.h"
+#import "Moonlight-Swift.h"
+@class Controller;
+
#define UPDATE_BUTTON(x, y) (buttonFlags = \
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
@@ -36,6 +38,7 @@
CALayer* _l1Button;
CALayer* _l2Button;
CALayer* _l3Button;
+ CALayer* _edge;
UITouch* _aTouch;
UITouch* _bTouch;
@@ -52,6 +55,7 @@
UITouch* _l1Touch;
UITouch* _l2Touch;
UITouch* _l3Touch;
+ UITouch* _edgeTouch;
NSDate* l3TouchStart;
NSDate* r3TouchStart;
@@ -63,12 +67,11 @@
CGRect _controlArea;
UIView* _view;
OnScreenControlsLevel _level;
- BOOL _visible;
ControllerSupport *_controllerSupport;
Controller *_controller;
+ id _edgeDelegate;
NSMutableArray* _deadTouches;
- BOOL _swapABXY;
}
static const float EDGE_WIDTH = .05;
@@ -112,15 +115,15 @@ static float L2_Y;
static float L3_X;
static float L3_Y;
-- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig {
+- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id)swipeDelegate {
self = [self init];
_view = view;
_controllerSupport = controllerSupport;
_controller = [controllerSupport getOscController];
+ _edgeDelegate = swipeDelegate;
_deadTouches = [[NSMutableArray alloc] init];
- _swapABXY = streamConfig.swapABXYButtons;
- _iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
+ _iPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
if (_iPad)
{
@@ -154,28 +157,16 @@ static float L3_Y;
_rightStickBackground = [CALayer layer];
_leftStick = [CALayer layer];
_rightStick = [CALayer layer];
+ _edge = [CALayer layer];
+
+ [self setupEdgeDetection];
return self;
}
-- (void) show {
- _visible = YES;
-
- [self updateControls];
-}
-
- (void) setLevel:(OnScreenControlsLevel)level {
_level = level;
-
- // Only update controls if we're showing, otherwise
- // show will do it for us.
- if (_visible) {
- [self updateControls];
- }
-}
-
-- (OnScreenControlsLevel) getLevel {
- return _level;
+ [self updateControls];
}
- (void) updateControls {
@@ -211,27 +202,15 @@ static float L3_Y;
[self hideSticks];
[self drawL3R3];
break;
- case OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons:
- // This variant of GCExtendedGamepad has L3 and R3 but
- // is still missing Select
- [self setupExtendedGamepadControls];
-
- [self hideButtons];
- [self hideBumpers];
- [self hideTriggers];
- [self hideL3R3];
- [self drawStartSelect];
- [self hideSticks];
- break;
case OnScreenControlsLevelSimple:
[self setupSimpleControls];
- [self hideTriggers];
- [self hideL3R3];
+ [self drawTriggers];
+ [self drawStartSelect];
+ [self drawL3R3];
+ [self hideButtons];
[self hideBumpers];
[self hideSticks];
- [self drawStartSelect];
- [self drawButtons];
break;
case OnScreenControlsLevelFull:
[self setupComplexControls];
@@ -249,27 +228,18 @@ static float L3_Y;
}
}
+- (void) setupEdgeDetection {
+ _edge.frame = CGRectMake(0, 0, _view.frame.size.width * EDGE_WIDTH, _view.frame.size.height);
+ [_view.layer addSublayer:_edge];
+}
+
// For GCExtendedGamepad controls we move start, select, L3, and R3 to the button
- (void) setupExtendedGamepadControls {
// Start with the default complex layout
[self setupComplexControls];
- START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
- SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
-
- L3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
- R3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
-
- if (_iPad) {
- L3_X = _controlArea.size.width * .15 + _controlArea.origin.x;
- R3_X = _controlArea.size.width * .85 + _controlArea.origin.x;
- }
- else {
- L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
- R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
- }
}
// For GCGamepad controls we move triggers, start, and select
@@ -278,21 +248,41 @@ static float L3_Y;
// Start with the default complex layout
[self setupComplexControls];
- L2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
- L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
+ START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
+ START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
- R2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
- R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
+ SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
+ SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
- START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
- START_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
- SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
- SELECT_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
+ L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+ L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
+
+ R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+ R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
if (_iPad) {
+ START_X = _controlArea.size.width * .75 + _controlArea.origin.x;
+ START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+
+ SELECT_X = _controlArea.size.width * .25 + _controlArea.origin.x;
+ SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+
// The analog sticks are kept closer to the sides on iPad
- LS_CENTER_X = _controlArea.size.width * .15 + _controlArea.origin.x;
- RS_CENTER_X = _controlArea.size.width * .85 + _controlArea.origin.x;
+ LS_CENTER_X = _controlArea.size.width * .18 + _controlArea.origin.x;
+ LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
+ RS_CENTER_X = _controlArea.size.width * .82 + _controlArea.origin.x;
+ RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
+ } else {
+ START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
+ START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+
+ SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
+ SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+
+ LS_CENTER_X = _controlArea.size.width * .25 + _controlArea.origin.x;
+ LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
+ RS_CENTER_X = _controlArea.size.width * .75 + _controlArea.origin.x;
+ RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
}
}
@@ -301,7 +291,10 @@ static float L3_Y;
// Start with the default complex layout
[self setupComplexControls];
+ START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+
+ SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
@@ -309,20 +302,6 @@ static float L3_Y;
R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
-
- if (_iPad) {
- // Lower the D-pad and buttons on iPad
- D_PAD_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
- BUTTON_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
-
- // Move Start and Select closer to sides
- SELECT_X = _controlArea.size.width * .2 + _controlArea.origin.x;
- START_X = _controlArea.size.width * .8 + _controlArea.origin.x;
- }
- else {
- SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
- START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
- }
}
- (void) setupComplexControls
@@ -336,9 +315,9 @@ static float L3_Y;
{
// The analog sticks are kept closer to the sides on iPad
LS_CENTER_X = _controlArea.size.width * .22 + _controlArea.origin.x;
- LS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
+ LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
RS_CENTER_X = _controlArea.size.width * .77 + _controlArea.origin.x;
- RS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
+ RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
}
else
{
@@ -353,54 +332,42 @@ static float L3_Y;
SELECT_X = _controlArea.size.width * .1 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
+ L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
L1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
+ L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
L2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
+ R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
R1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
+ R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
R2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
-
- if (_iPad) {
- // Move L/R buttons closer to the side on iPad
- L1_X = _controlArea.size.width * .05 + _controlArea.origin.x;
- L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
- R1_X = _controlArea.size.width * .95 + _controlArea.origin.x;
- R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
- }
- else {
- L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
- L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
- R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
- R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
- }
+ L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
+ L3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
+ R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
+ R3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
}
- (void) drawButtons {
- UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
- UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
- UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
- UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
-
- CGRect aButtonFrame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
- CGRect bButtonFrame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
- CGRect xButtonFrame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
- CGRect yButtonFrame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
-
// create A button
+ UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
_aButton.contents = (id) aButtonImage.CGImage;
- _aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame;
+ _aButton.frame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
[_view.layer addSublayer:_aButton];
// create B button
- _bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame;
+ UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
+ _bButton.frame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
_bButton.contents = (id) bButtonImage.CGImage;
[_view.layer addSublayer:_bButton];
// create X Button
- _xButton.frame = _swapABXY ? yButtonFrame : xButtonFrame;
+ UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
+ _xButton.frame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
_xButton.contents = (id) xButtonImage.CGImage;
[_view.layer addSublayer:_xButton];
// create Y Button
- _yButton.frame = _swapABXY ? xButtonFrame : yButtonFrame;
+ UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
+ _yButton.frame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
_yButton.contents = (id) yButtonImage.CGImage;
[_view.layer addSublayer:_yButton];
@@ -654,7 +621,7 @@ static float L3_Y;
if (updated) {
[_controllerSupport updateFinished:_controller];
}
- return updated || buttonTouch;
+ return updated || buttonTouch;
}
- (BOOL)handleTouchDownEvent:touches {
@@ -663,63 +630,63 @@ static float L3_Y;
for (UITouch* touch in touches) {
CGPoint touchLocation = [touch locationInView:_view];
- if (_aButton.superlayer != nil && [_aButton.presentationLayer hitTest:touchLocation]) {
+ if ([_aButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:A_FLAG];
_aTouch = touch;
updated = true;
- } else if (_bButton.superlayer != nil && [_bButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_bButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:B_FLAG];
_bTouch = touch;
updated = true;
- } else if (_xButton.superlayer != nil && [_xButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_xButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:X_FLAG];
_xTouch = touch;
updated = true;
- } else if (_yButton.superlayer != nil && [_yButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_yButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:Y_FLAG];
_yTouch = touch;
updated = true;
- } else if (_upButton.superlayer != nil && [_upButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_upButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
_dpadTouch = touch;
updated = true;
- } else if (_downButton.superlayer != nil && [_downButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_downButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
_dpadTouch = touch;
updated = true;
- } else if (_leftButton.superlayer != nil && [_leftButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_leftButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
_dpadTouch = touch;
updated = true;
- } else if (_rightButton.superlayer != nil && [_rightButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_rightButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
_dpadTouch = touch;
updated = true;
- } else if (_startButton.superlayer != nil && [_startButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_startButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG];
_startTouch = touch;
updated = true;
- } else if (_selectButton.superlayer != nil && [_selectButton.presentationLayer hitTest:touchLocation]) {
+ } else if ([_selectButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:BACK_FLAG];
_selectTouch = touch;
updated = true;
- } else if (_l1Button.superlayer != nil && [_l1Button.presentationLayer hitTest:touchLocation]) {
+ } else if ([_l1Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:LB_FLAG];
_l1Touch = touch;
updated = true;
- } else if (_r1Button.superlayer != nil && [_r1Button.presentationLayer hitTest:touchLocation]) {
+ } else if ([_r1Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:RB_FLAG];
_r1Touch = touch;
updated = true;
- } else if (_l2Button.superlayer != nil && [_l2Button.presentationLayer hitTest:touchLocation]) {
+ } else if ([_l2Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport updateLeftTrigger:_controller left:0xFF];
_l2Touch = touch;
updated = true;
- } else if (_r2Button.superlayer != nil && [_r2Button.presentationLayer hitTest:touchLocation]) {
+ } else if ([_r2Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport updateRightTrigger:_controller right:0xFF];
_r2Touch = touch;
updated = true;
- } else if (_l3Button.superlayer != nil && [_l3Button.presentationLayer hitTest:touchLocation]) {
+ } else if ([_l3Button.presentationLayer hitTest:touchLocation]) {
if (l3Set) {
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
_l3Button.borderWidth = 0.0f;
@@ -730,7 +697,7 @@ static float L3_Y;
l3Set = !l3Set;
_l3Touch = touch;
updated = true;
- } else if (_r3Button.superlayer != nil && [_r3Button.presentationLayer hitTest:touchLocation]) {
+ } else if ([_r3Button.presentationLayer hitTest:touchLocation]) {
if (r3Set) {
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
_r3Button.borderWidth = 0.0f;
@@ -741,7 +708,7 @@ static float L3_Y;
r3Set = !r3Set;
_r3Touch = touch;
updated = true;
- } else if (_leftStick.superlayer != nil && [_leftStick.presentationLayer hitTest:touchLocation]) {
+ } else if ([_leftStick.presentationLayer hitTest:touchLocation]) {
if (l3TouchStart != nil) {
// Find elapsed time and convert to milliseconds
// Use (-) modifier to conversion since receiver is earlier than now
@@ -753,7 +720,7 @@ static float L3_Y;
}
_lsTouch = touch;
stickTouch = true;
- } else if (_rightStick.superlayer != nil && [_rightStick.presentationLayer hitTest:touchLocation]) {
+ } else if ([_rightStick.presentationLayer hitTest:touchLocation]) {
if (r3TouchStart != nil) {
// Find elapsed time and convert to milliseconds
// Use (-) modifier to conversion since receiver is earlier than now
@@ -765,6 +732,8 @@ static float L3_Y;
}
_rsTouch = touch;
stickTouch = true;
+ } else if ([_edge.presentationLayer hitTest:touchLocation]) {
+ _edgeTouch = touch;
}
if (!updated && !stickTouch && [self isInDeadZone:touch]) {
[_deadTouches addObject:touch];
@@ -848,6 +817,11 @@ static float L3_Y;
else if (touch == _r3Touch) {
_r3Touch = nil;
touched = true;
+ } else if (touch == _edgeTouch) {
+ _edgeTouch = nil;
+ if ([touch locationInView:_view].x >= _view.frame.size.width / 4) {
+ [_edgeDelegate edgeSwiped];
+ }
}
if ([_deadTouches containsObject:touch]) {
[_deadTouches removeObject:touch];
@@ -862,31 +836,26 @@ static float L3_Y;
}
- (BOOL) isInDeadZone:(UITouch*) touch {
- // Dynamically evaluate deadzones based on the controls
- // on screen at the time
- if (_leftButton.superlayer != nil && [self isDpadDeadZone:touch]) {
- return true;
+ switch (_level) {
+ case OnScreenControlsLevelFull:
+ return [self isDpadDeadZone:touch]
+ || [self isAbxyDeadZone:touch]
+ || [self isTriggerDeadZone:touch]
+ || [self isBumperDeadZone:touch]
+ || [self isStartSelectDeadZone:touch];
+ case OnScreenControlsLevelSimple:
+ return [self isTriggerDeadZone:touch]
+ || [self isStartSelectDeadZone:touch]
+ || [self isL3R3DeadZone:touch];
+ case OnScreenControlsLevelAutoGCExtendedGamepad:
+ return [self isL3R3DeadZone:touch]
+ || [self isStartSelectDeadZone:touch];
+ case OnScreenControlsLevelAutoGCGamepad:
+ return [self isTriggerDeadZone:touch]
+ || [self isStartSelectDeadZone:touch];
+ default:
+ return false;
}
- else if (_aButton.superlayer != nil && [self isAbxyDeadZone:touch]) {
- return true;
- }
- else if (_l2Button.superlayer != nil && [self isTriggerDeadZone:touch]) {
- return true;
- }
- else if (_l1Button.superlayer != nil && [self isBumperDeadZone:touch]) {
- return true;
- }
- else if (_startButton.superlayer != nil && [self isStartSelectDeadZone:touch]) {
- return true;
- }
- else if (_l3Button.superlayer != nil && [self isL3R3DeadZone:touch]) {
- return true;
- }
- else if (_leftStickBackground.superlayer != nil && [self isStickDeadZone:touch]) {
- return true;
- }
-
- return false;
}
- (BOOL) isDpadDeadZone:(UITouch*) touch {
@@ -957,19 +926,6 @@ static float L3_Y;
endY:_view.frame.origin.y + _view.frame.size.height];
}
-- (BOOL) isStickDeadZone:(UITouch*) touch {
- return [self isDeadZone:touch
- startX:_leftStickBackground.frame.origin.x - 15
- startY:_leftStickBackground.frame.origin.y - 15
- endX:_leftStickBackground.frame.origin.x + _leftStickBackground.frame.size.width + 15
- endY:_view.frame.origin.y + _view.frame.size.height]
- || [self isDeadZone:touch
- startX:_rightStickBackground.frame.origin.x - 15
- startY:_rightStickBackground.frame.origin.y - 15
- endX:_rightStickBackground.frame.origin.x + _rightStickBackground.frame.size.width + 15
- endY:_view.frame.origin.y + _view.frame.size.height];
-}
-
- (BOOL) isDeadZone:(UITouch*) touch startX:(float)deadZoneStartX startY:(float)deadZoneStartY endX:(float)deadZoneEndX endY:(float)deadZoneEndY {
deadZoneStartX -= DEAD_ZONE_PADDING;
deadZoneStartY -= DEAD_ZONE_PADDING;
diff --git a/Limelight/Input/RelativeTouchHandler.h b/Limelight/Input/RelativeTouchHandler.h
deleted file mode 100644
index 2f8d9db..0000000
--- a/Limelight/Input/RelativeTouchHandler.h
+++ /dev/null
@@ -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
diff --git a/Limelight/Input/RelativeTouchHandler.m b/Limelight/Input/RelativeTouchHandler.m
deleted file mode 100644
index 64d7da4..0000000
--- a/Limelight/Input/RelativeTouchHandler.m
+++ /dev/null
@@ -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
-
-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
diff --git a/Limelight/Input/StreamView.h b/Limelight/Input/StreamView.h
index 9f50010..b18c813 100644
--- a/Limelight/Input/StreamView.h
+++ b/Limelight/Input/StreamView.h
@@ -6,32 +6,18 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "ControllerSupport.h"
-#import "OnScreenControls.h"
-#import "Moonlight-Swift.h"
-#import "StreamConfiguration.h"
-@protocol UserInteractionDelegate
+@protocol EdgeDetectionDelegate
-- (void) userInteractionBegan;
-- (void) userInteractionEnded;
+- (void) edgeSwiped;
@end
-#if TARGET_OS_TV
-@interface StreamView : UIView
-#else
-@interface StreamView : UIView
-#endif
+@interface StreamView : UIView
-- (void) setupStreamView:(ControllerSupport*)controllerSupport
- interactionDelegate:(id)interactionDelegate
- config:(StreamConfiguration*)streamConfig;
-- (void) showOnScreenControls;
-- (OnScreenControlsLevel) getCurrentOscState;
-
-#if !TARGET_OS_TV
-- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
-#endif
+- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id)swipeDelegate;
+- (void) setMouseDeltaFactors:(float)x y:(float)y;
@end
diff --git a/Limelight/Input/StreamView.m b/Limelight/Input/StreamView.m
index 7dedeb3..bd4afa2 100644
--- a/Limelight/Input/StreamView.m
+++ b/Limelight/Input/StreamView.m
@@ -8,951 +8,142 @@
#import "StreamView.h"
#include
+#import "OnScreenControls.h"
#import "DataManager.h"
#import "ControllerSupport.h"
-#import "KeyboardSupport.h"
-#import "RelativeTouchHandler.h"
-#import "AbsoluteTouchHandler.h"
-#import "KeyboardInputField.h"
-
-static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
@implementation StreamView {
+ CGPoint touchLocation;
+ BOOL touchMoved;
OnScreenControls* onScreenControls;
- KeyboardInputField* keyInputField;
- BOOL isInputingText;
- NSMutableSet* keysDown;
+ BOOL isDragging;
+ NSTimer* dragTimer;
- float streamAspectRatio;
-
- // iOS 13.4 mouse support
- NSInteger lastMouseButtonMask;
- float lastMouseX;
- float lastMouseY;
- CGPoint lastScrollTranslation;
-
- // Citrix X1 mouse support
- X1Mouse* x1mouse;
- double accumulatedMouseDeltaX;
- double accumulatedMouseDeltaY;
-
- UIResponder* touchHandler;
-
- id interactionDelegate;
- NSTimer* interactionTimer;
- BOOL hasUserInteracted;
-
- NSDictionary *dictCodes;
+ float xDeltaFactor;
+ float yDeltaFactor;
+ float screenFactor;
}
-- (void) setupStreamView:(ControllerSupport*)controllerSupport
- interactionDelegate:(id)interactionDelegate
- config:(StreamConfiguration*)streamConfig {
- self->interactionDelegate = interactionDelegate;
- self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
+- (void) setMouseDeltaFactors:(float)x y:(float)y {
+ xDeltaFactor = x;
+ yDeltaFactor = y;
- TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
+ screenFactor = [[UIScreen mainScreen] scale];
+}
+
+- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id)swipeDelegate {
+ onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport swipeDelegate:swipeDelegate];
+ DataManager* dataMan = [[DataManager alloc] init];
+ OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
- keysDown = [[NSMutableSet alloc] init];
- keyInputField = [[KeyboardInputField alloc] initWithFrame:CGRectZero];
- [keyInputField setKeyboardType:UIKeyboardTypeDefault];
- [keyInputField setAutocorrectionType:UITextAutocorrectionTypeNo];
- [keyInputField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
- [keyInputField setSpellCheckingType:UITextSpellCheckingTypeNo];
- [self addSubview:keyInputField];
-
-#if TARGET_OS_TV
- // tvOS requires RelativeTouchHandler to manage Apple Remote input
- self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
-#else
- // iOS uses RelativeTouchHandler or AbsoluteTouchHandler depending on user preference
- if (settings.absoluteTouchMode) {
- self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self];
- }
- else {
- self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
- }
-
- onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport streamConfig:streamConfig];
- OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
- if (settings.absoluteTouchMode) {
- Log(LOG_I, @"On-screen controls disabled in absolute touch mode");
- [onScreenControls setLevel:OnScreenControlsLevelOff];
- }
- else if (level == OnScreenControlsLevelAuto) {
+ if (level == OnScreenControlsLevelAuto) {
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
}
else {
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
[onScreenControls setLevel:level];
}
-
- // It would be nice to just use GCMouse on iOS 14+ and the older API on iOS 13
- // but unfortunately that isn't possible today. GCMouse doesn't recognize many
- // mice correctly, but UIKit does. We will register for both and ignore UIKit
- // events if a GCMouse is connected.
- if (@available(iOS 13.4, *)) {
- [self addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
-
- UIPanGestureRecognizer *discreteMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedDiscrete:)];
- discreteMouseWheelRecognizer.maximumNumberOfTouches = 0;
- discreteMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
- discreteMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
- [self addGestureRecognizer:discreteMouseWheelRecognizer];
-
- UIPanGestureRecognizer *continuousMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedContinuous:)];
- continuousMouseWheelRecognizer.maximumNumberOfTouches = 0;
- continuousMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
- continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
- [self addGestureRecognizer:continuousMouseWheelRecognizer];
- }
-
-#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
- if (@available(iOS 16.1, *)) {
- UIHoverGestureRecognizer *stylusHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(sendStylusHoverEvent:)];
- stylusHoverRecognizer.allowedTouchTypes = @[@(UITouchTypePencil)];
- [self addGestureRecognizer:stylusHoverRecognizer];
- }
-#endif
-#endif
-
- x1mouse = [[X1Mouse alloc] init];
- x1mouse.delegate = self;
-
- if (settings.btMouseSupport) {
- [x1mouse start];
- }
-
- // This is critical to ensure keyboard events are delivered to this
- // StreamView and not our parent UIView, especially on tvOS.
- [self becomeFirstResponder];
}
-- (void)startInteractionTimer {
- // Restart user interaction tracking
- hasUserInteracted = NO;
-
- BOOL timerAlreadyRunning = interactionTimer != nil;
-
- // Start/restart the timer
- [interactionTimer invalidate];
- interactionTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
- target:self
- selector:@selector(interactionTimerExpired:)
- userInfo:nil
- repeats:NO];
-
- // Notify the delegate if this was a new user interaction
- if (!timerAlreadyRunning) {
- [interactionDelegate userInteractionBegan];
- }
-}
-
-- (void)interactionTimerExpired:(NSTimer *)timer {
- if (!hasUserInteracted) {
- // User has finished touching the screen
- interactionTimer = nil;
- [interactionDelegate userInteractionEnded];
- }
- else {
- // User is still touching the screen. Restart the timer.
- [self startInteractionTimer];
- }
-}
-
-- (void) showOnScreenControls {
-#if !TARGET_OS_TV
- [onScreenControls show];
-#endif
-}
-
-- (OnScreenControlsLevel) getCurrentOscState {
- if (onScreenControls == nil) {
- return OnScreenControlsLevelOff;
- }
- else {
- return [onScreenControls getLevel];
- }
-}
-
-- (CGSize) getVideoAreaSize {
- if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
- return CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
- } else {
- return CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
- }
-}
-
-- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point {
- // These are now relative to the StreamView, however we need to scale them
- // further to make them relative to the actual video portion.
- float x = point.x - self.bounds.origin.x;
- float y = point.y - self.bounds.origin.y;
-
- // For some reason, we don't seem to always get to the bounds of the window
- // so we'll subtract 1 pixel if we're to the left/below of the origin and
- // and add 1 pixel if we're to the right/above. It should be imperceptible
- // to the user but it will allow activation of gestures that require contact
- // with the edge of the screen (like Aero Snap).
- if (x < self.bounds.size.width / 2) {
- x--;
- }
- else {
- x++;
- }
- if (y < self.bounds.size.height / 2) {
- y--;
- }
- else {
- y++;
- }
-
- // This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
- CGSize videoSize = [self getVideoAreaSize];
- CGPoint videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
- self.bounds.size.height / 2 - videoSize.height / 2);
-
- // Confine the cursor to the video region. We don't just discard events outside
- // the region because we won't always get one exactly when the mouse leaves the region.
- return CGPointMake(MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x,
- MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y);
-}
-
-#if !TARGET_OS_TV
-
-- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle {
- // iOS reports azimuth of 0 when the stylus is pointing west, but Moonlight expects
- // rotation of 0 to mean the stylus is pointing north. Rotate the azimuth angle
- // clockwise by 90 degrees to convert from iOS to Moonlight rotation conventions.
- int32_t rotationAngle = (azimuthAngle - M_PI_2) * (180.f / M_PI);
- if (rotationAngle < 0) {
- rotationAngle += 360;
- }
- return (uint16_t)rotationAngle;
-}
-
-- (uint8_t)getTiltFromAltitudeAngle:(float)altitudeAngle {
- // iOS reports an altitude of 0 when the stylus is parallel to the touch surface,
- // while Moonlight expects a tilt of 0 when the stylus is perpendicular to the surface.
- // Subtract the tilt angle from 90 to convert from iOS to Moonlight tilt conventions.
- uint8_t altitudeDegs = abs((int16_t)(altitudeAngle * (180.f / M_PI)));
- return 90 - MIN(90, altitudeDegs);
-}
-
-- (BOOL)sendStylusEvent:(UITouch*)event {
- uint8_t type;
-
- // Don't touch stylus events if the host doesn't support them. We want to pass
- // them as normal touches for legacy hosts that don't understand pen events.
- if (!(LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS)) {
- return NO;
- }
-
- switch (event.phase) {
- case UITouchPhaseBegan:
- type = LI_TOUCH_EVENT_DOWN;
- break;
- case UITouchPhaseMoved:
- type = LI_TOUCH_EVENT_MOVE;
- break;
- case UITouchPhaseEnded:
- type = LI_TOUCH_EVENT_UP;
- break;
- case UITouchPhaseCancelled:
- type = LI_TOUCH_EVENT_CANCEL;
- break;
- default:
- return YES;
- }
-
- CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
- CGSize videoSize = [self getVideoAreaSize];
-
- return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
- (event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
- 0.0f, 0.0f,
- [self getRotationFromAzimuthAngle:[event azimuthAngleInView:self]],
- [self getTiltFromAltitudeAngle:event.altitudeAngle]) != LI_ERR_UNSUPPORTED;
-}
-
-- (void)sendStylusHoverEvent:(UIHoverGestureRecognizer*)gesture API_AVAILABLE(ios(13.0)) {
- uint8_t type;
-
- switch (gesture.state) {
- case UIGestureRecognizerStateBegan:
- case UIGestureRecognizerStateChanged:
- type = LI_TOUCH_EVENT_HOVER;
- break;
-
- case UIGestureRecognizerStateEnded:
- type = LI_TOUCH_EVENT_HOVER_LEAVE;
- break;
-
- default:
- return;
- }
-
- CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
- CGSize videoSize = [self getVideoAreaSize];
-
- float distance = 0.0f;
-#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
- if (@available(iOS 16.1, *)) {
- distance = gesture.zOffset;
- }
-#endif
-
- uint16_t rotationAngle = LI_ROT_UNKNOWN;
- uint8_t tiltAngle = LI_TILT_UNKNOWN;
-#if defined(__IPHONE_16_4) || defined(__TVOS_16_4)
- if (@available(iOS 16.4, *)) {
- rotationAngle = [self getRotationFromAzimuthAngle:[gesture azimuthAngleInView:self]];
- tiltAngle = [self getTiltFromAltitudeAngle:gesture.altitudeAngle];
- }
-#endif
-
- LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
- distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
-}
-
-#endif
-
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
- if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
- forTouches:touches
- withEvent:event]) {
- // If it's a mouse event, we're done
- return;
- }
-
Log(LOG_D, @"Touch down");
-
- // Notify of user interaction and start expiration timer
- [self startInteractionTimer];
-
-#if !TARGET_OS_TV
- if (@available(iOS 13.4, *)) {
- for (UITouch* touch in touches) {
- if (touch.type == UITouchTypePencil) {
- if ([self sendStylusEvent:touch]) {
- return;
- }
- }
- }
- }
-#endif
-
if (![onScreenControls handleTouchDownEvent:touches]) {
- // We still inform the touch handler even if we're going trigger the
- // keyboard activation gesture. This is important to ensure the touch
- // handler has a consistent view of touch events to correctly suppress
- // activation of one or two finger gestures when a three finger gesture
- // is triggered.
- [touchHandler touchesBegan:touches withEvent:event];
-
- if ([[event allTouches] count] == 3) {
- if (isInputingText) {
- Log(LOG_D, @"Closing the keyboard");
- [keyInputField resignFirstResponder];
- isInputingText = false;
- } else {
- Log(LOG_D, @"Opening the keyboard");
- // Prepare the textbox used to capture keyboard events.
- keyInputField.delegate = self;
- keyInputField.text = @"0";
-#if !TARGET_OS_TV
- // Prepare the toolbar above the keyboard for more options
- UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 44)];
-
- UIBarButtonItem *doneBarButton = [self createButtonWithImageNamed:@"DoneIcon.png" backgroundColor:[UIColor clearColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x00 isToggleable:NO];
- UIBarButtonItem *windowsBarButton = [self createButtonWithImageNamed:@"WindowsIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x5B isToggleable:YES];
- UIBarButtonItem *tabBarButton = [self createButtonWithImageNamed:@"TabIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x09 isToggleable:NO];
- UIBarButtonItem *shiftBarButton = [self createButtonWithImageNamed:@"ShiftIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA0 isToggleable:YES];
- UIBarButtonItem *escapeBarButton = [self createButtonWithImageNamed:@"EscapeIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x1B isToggleable:NO];
- UIBarButtonItem *controlBarButton = [self createButtonWithImageNamed:@"ControlIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA2 isToggleable:YES];
- UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA4 isToggleable:YES];
- UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO];
- UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
-
- [customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
- keyInputField.inputAccessoryView = customToolbarView;
-#endif
- [keyInputField becomeFirstResponder];
- [keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
-
- // Undo causes issues for our state management, so turn it off
- [keyInputField.undoManager disableUndoRegistration];
-
- isInputingText = true;
- }
+ UITouch *touch = [[event allTouches] anyObject];
+ touchLocation = [touch locationInView:self];
+ touchMoved = false;
+ if ([[event allTouches] count] == 1 && !isDragging) {
+ dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650
+ target:self
+ selector:@selector(onDragStart:)
+ userInfo:nil
+ repeats:NO];
}
}
}
-- (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable {
- UIImage *image = [UIImage imageNamed:imageName];
- UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
- [button setImage:image forState:UIControlStateNormal];
- button.frame = CGRectMake(0, 0, 30, 30);
- button.imageView.contentMode = UIViewContentModeScaleAspectFit;
- button.imageView.backgroundColor = backgroundColor;
- button.imageView.layer.cornerRadius = 10.0;
- button.imageEdgeInsets = UIEdgeInsetsMake(6, 6, 6, 6);
- [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
- objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- objc_setAssociatedObject(button, "isToggleable", @(isToggleable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- objc_setAssociatedObject(button, "isOn", @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
- return barButton;
-}
-
-- (void)toolbarButtonClicked:(UIButton *)sender {
- BOOL isToggleable = [objc_getAssociatedObject(sender, "isToggleable") boolValue];
- BOOL isOn = [objc_getAssociatedObject(sender, "isOn") boolValue];
- if (isToggleable){
- isOn = !isOn;
- // Update the button's appearance based on its new state
- if (isOn) {
- sender.imageView.backgroundColor = [UIColor lightGrayColor];
- } else {
- sender.imageView.backgroundColor = [UIColor blackColor];
- }
+- (void)onDragStart:(NSTimer*)timer {
+ if (!touchMoved && !isDragging){
+ isDragging = true;
+ LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
- // Update the new on/off state of the button
- objc_setAssociatedObject(sender, "isOn", @(isOn), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- // Get the keyCode parameter and convert to short for key press event
- short keyCode = [objc_getAssociatedObject(sender, "keyCode") shortValue];
- // Close keyboard if done button clicked
- if (!keyCode) {
- [keyInputField resignFirstResponder];
- isInputingText = false;
- }
- else {
- // Send key press event using keyCode parameter, toggle if necessary
- if (isToggleable){
- if (isOn){
- LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
- [keysDown addObject:@(keyCode)];
- } else {
- LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
- [keysDown removeObject:@(keyCode)];
- }
- }
- else {
- LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
- usleep(50 * 1000);
- LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
- }
- }
-}
-
-- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
-#if !TARGET_OS_TV
- if (@available(iOS 13.4, *)) {
- UITouch* touch = [touches anyObject];
- if (touch.type == UITouchTypeIndirectPointer) {
- if (@available(iOS 14.0, *)) {
- if ([GCMouse current] != nil) {
- // We'll handle this with GCMouse. Do nothing here.
- return YES;
- }
- }
-
- UIEventButtonMask normalizedButtonMask;
-
- // iOS 14 includes the released button in the buttonMask for the release
- // event, while iOS 13 does not. Normalize that behavior here.
- if (@available(iOS 14.0, *)) {
- if (buttonAction == BUTTON_ACTION_RELEASE) {
- normalizedButtonMask = lastMouseButtonMask & ~event.buttonMask;
- }
- else {
- normalizedButtonMask = event.buttonMask;
- }
- }
- else {
- normalizedButtonMask = event.buttonMask;
- }
-
- UIEventButtonMask changedButtons = lastMouseButtonMask ^ normalizedButtonMask;
-
- for (int i = BUTTON_LEFT; i <= BUTTON_X2; i++) {
- UIEventButtonMask buttonFlag;
-
- switch (i) {
- // Right and Middle are reversed from what iOS uses
- case BUTTON_RIGHT:
- buttonFlag = UIEventButtonMaskForButtonNumber(2);
- break;
- case BUTTON_MIDDLE:
- buttonFlag = UIEventButtonMaskForButtonNumber(3);
- break;
-
- default:
- buttonFlag = UIEventButtonMaskForButtonNumber(i);
- break;
- }
-
- if (changedButtons & buttonFlag) {
- LiSendMouseButtonEvent(buttonAction, i);
- }
- }
-
- lastMouseButtonMask = normalizedButtonMask;
- return YES;
- }
- }
-#endif
-
- return NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
-#if !TARGET_OS_TV
- if (@available(iOS 13.4, *)) {
- for (UITouch* touch in touches) {
- if (touch.type == UITouchTypePencil) {
- if ([self sendStylusEvent:touch]) {
- return;
- }
- }
- }
-
- UITouch *touch = [touches anyObject];
- if (touch.type == UITouchTypeIndirectPointer) {
- if (@available(iOS 14.0, *)) {
- if ([GCMouse current] != nil) {
- // We'll handle this with GCMouse. Do nothing here.
- return;
- }
- }
-
- // We must handle this event to properly support
- // drags while the middle, X1, or X2 mouse buttons are
- // held down. For some reason, left and right buttons
- // don't require this, but we do it anyway for them too.
- // Cursor movement without a button held down is handled
- // in pointerInteraction:regionForRequest:defaultRegion.
- [self updateCursorLocation:[touch locationInView:self] isMouse:YES];
- return;
- }
- }
-#endif
-
- hasUserInteracted = YES;
-
if (![onScreenControls handleTouchMovedEvent:touches]) {
- [touchHandler touchesMoved:touches withEvent:event];
- }
-}
-
-- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event {
- BOOL handled = NO;
-
- if (@available(iOS 13.4, tvOS 13.4, *)) {
- for (UIPress* press in presses) {
- // For now, we'll treated it as handled if we handle at least one of the
- // UIPress events inside the set.
- if ([KeyboardSupport sendKeyEventForPress:press down:YES]) {
- // This will prevent the legacy UITextField from receiving the event
- handled = YES;
+ [dragTimer invalidate];
+ dragTimer = nil;
+ if ([[event allTouches] count] == 1) {
+ UITouch *touch = [[event allTouches] anyObject];
+ CGPoint currentLocation = [touch locationInView:self];
+
+ if (touchLocation.x != currentLocation.x ||
+ touchLocation.y != currentLocation.y)
+ {
+ int deltaX = currentLocation.x - touchLocation.x;
+ int deltaY = currentLocation.y - touchLocation.y;
+
+ deltaX *= xDeltaFactor * screenFactor;
+ deltaY *= yDeltaFactor * screenFactor;
+
+ if (deltaX != 0 || deltaY != 0) {
+ LiSendMouseMoveEvent(deltaX, deltaY);
+ touchMoved = true;
+ touchLocation = currentLocation;
+ }
}
+ } else if ([[event allTouches] count] == 2) {
+ CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:self];
+ CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:self];
+
+ CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
+ if (touchLocation.y != avgLocation.y) {
+ LiSendScrollEvent(avgLocation.y - touchLocation.y);
+ }
+ touchMoved = true;
+ touchLocation = avgLocation;
}
}
- if (!handled) {
- [super pressesBegan:presses withEvent:event];
- }
-}
-
-- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event {
- BOOL handled = NO;
-
- if (@available(iOS 13.4, tvOS 13.4, *)) {
- for (UIPress* press in presses) {
- // For now, we'll treated it as handled if we handle at least one of the
- // UIPress events inside the set.
- if ([KeyboardSupport sendKeyEventForPress:press down:NO]) {
- // This will prevent the legacy UITextField from receiving the event
- handled = YES;
- }
- }
- }
-
- if (!handled) {
- [super pressesEnded:presses withEvent:event];
- }
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
- if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
- forTouches:touches
- withEvent:event]) {
- // If it's a mouse event, we're done
- return;
- }
-
Log(LOG_D, @"Touch up");
-
- hasUserInteracted = YES;
-
-#if !TARGET_OS_TV
- if (@available(iOS 13.4, *)) {
- for (UITouch* touch in touches) {
- if (touch.type == UITouchTypePencil) {
- if ([self sendStylusEvent:touch]) {
- return;
+ if (![onScreenControls handleTouchUpEvent:touches]) {
+ [dragTimer invalidate];
+ dragTimer = nil;
+ if (!touchMoved) {
+ if ([[event allTouches] count] == 2) {
+ 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 (!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);
}
+ isDragging = false;
+ LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
}
- }
-#endif
-
- if (![onScreenControls handleTouchUpEvent:touches]) {
- [touchHandler touchesEnded:touches withEvent:event];
+ else if (isDragging) {
+ isDragging = false;
+ LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
+ }
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
- [touchHandler touchesCancelled:touches withEvent:event];
- [self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
- forTouches:touches
- withEvent:event];
-#if !TARGET_OS_TV
- if (@available(iOS 13.4, *)) {
- for (UITouch* touch in touches) {
- if (touch.type == UITouchTypePencil) {
- [self sendStylusEvent:touch];
- }
- }
- }
-#endif
}
-#if !TARGET_OS_TV
-- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse {
- CGPoint normalizedLocation = [self adjustCoordinatesForVideoArea:location];
- CGSize videoSize = [self getVideoAreaSize];
-
- // Send the mouse position relative to the video region if it has changed
- // if we're receiving coordinates from a real mouse.
- //
- // NB: It is important for functionality (not just optimization) to only
- // send it if the value has changed. We will receive one of these events
- // any time the user presses a modifier key, which can result in errant
- // mouse motion when using a Citrix X1 mouse.
- if (normalizedLocation.x != lastMouseX || normalizedLocation.y != lastMouseY || !isMouse) {
- if (lastMouseX != 0 || lastMouseY != 0 || !isMouse) {
- LiSendMousePositionEvent(normalizedLocation.x, normalizedLocation.y, videoSize.width, videoSize.height);
- }
-
- if (isMouse) {
- lastMouseX = normalizedLocation.x;
- lastMouseY = normalizedLocation.y;
- }
- }
-}
-
-- (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
- regionForRequest:(UIPointerRegionRequest *)request
- defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4)) {
- if (@available(iOS 14.0, *)) {
- if ([GCMouse current] != nil) {
- // We'll handle this with GCMouse. Do nothing here.
- return nil;
- }
- }
-
- // This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
- CGSize videoSize;
- CGPoint videoOrigin;
- if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
- videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
- } else {
- videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
- }
- videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
- self.bounds.size.height / 2 - videoSize.height / 2);
-
- // Move the cursor on the host if no buttons are pressed.
- // Motion with buttons pressed in handled in touchesMoved:
- if (lastMouseButtonMask == 0) {
- [self updateCursorLocation:request.location isMouse:YES];
- }
-
- // The pointer interaction should cover the video region only
- return [UIPointerRegion regionWithRect:CGRectMake(videoOrigin.x, videoOrigin.y, videoSize.width, videoSize.height) identifier:nil];
-}
-
-- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
- // Always hide the mouse cursor over our stream view
- return [UIPointerStyle hiddenPointerStyle];
-}
-
-- (void)mouseWheelMovedContinuous:(UIPanGestureRecognizer *)gesture {
- switch (gesture.state) {
- case UIGestureRecognizerStateBegan:
- case UIGestureRecognizerStateChanged:
- break;
-
- case UIGestureRecognizerStateEnded:
- default:
- // Ignore recognition failure and other states
- lastScrollTranslation = CGPointMake(0, 0);
- return;
- }
-
- CGPoint currentScrollTranslation = [gesture translationInView:self];
- const short translationMultiplier = 120 * 20; // WHEEL_DELTA * 20
-
- {
- short translationDeltaY = ((currentScrollTranslation.y - lastScrollTranslation.y) / self.bounds.size.height) * translationMultiplier;
- if (translationDeltaY != 0) {
- LiSendHighResScrollEvent(translationDeltaY);
- lastScrollTranslation = currentScrollTranslation;
- }
- }
-
- {
- short translationDeltaX = ((currentScrollTranslation.x - lastScrollTranslation.x) / self.bounds.size.width) * translationMultiplier;
- if (translationDeltaX != 0) {
- // Direction is reversed from vertical scrolling
- LiSendHighResHScrollEvent(-translationDeltaX);
- lastScrollTranslation = currentScrollTranslation;
- }
- }
-}
-
-- (void)mouseWheelMovedDiscrete:(UIPanGestureRecognizer *)gesture {
- switch (gesture.state) {
- case UIGestureRecognizerStateBegan:
- case UIGestureRecognizerStateChanged:
- break;
-
- case UIGestureRecognizerStateEnded:
- default:
- // Ignore recognition failure and other states
- lastScrollTranslation = CGPointMake(0, 0);
- return;
- }
-
- // Using velocityInView is 0 for discrete scroll events
- // when scrolling very slowly, but translationInView does work.
- CGPoint currentScrollTranslation = [gesture translationInView:self];
-
- {
- short translationDeltaY = currentScrollTranslation.y - lastScrollTranslation.y;
- if (translationDeltaY != 0) {
- LiSendScrollEvent(translationDeltaY > 0 ? 1 : -1);
- }
- }
-
- {
- short translationDeltaX = currentScrollTranslation.x - lastScrollTranslation.x;
- if (translationDeltaX != 0) {
- // Direction is reversed from vertical scrolling
- LiSendHScrollEvent(translationDeltaX < 0 ? 1 : -1);
- }
- }
-
- lastScrollTranslation = currentScrollTranslation;
-}
-
-#endif
-
-- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
- if (@available(iOS 13.0, *)) {
- // Disable the 3 finger tap gestures that trigger the copy/paste/undo toolbar on iOS 13+
- return gestureRecognizer.name == nil || ![gestureRecognizer.name hasPrefix:@"kbProductivity."];
- }
- else {
- return YES;
- }
-}
-
-- (BOOL)textFieldShouldReturn:(UITextField *)textField {
- // This method is called when the "Return" key is pressed.
- LiSendKeyboardEvent(0x0d, KEY_ACTION_DOWN, 0);
- usleep(50 * 1000);
- LiSendKeyboardEvent(0x0d, KEY_ACTION_UP, 0);
- return NO;
-}
-
-- (void)textFieldDidEndEditing:(UITextField *)textField {
- for (NSNumber* keyCode in keysDown) {
- LiSendKeyboardEvent([keyCode shortValue], KEY_ACTION_UP, 0);
- }
- [keysDown removeAllObjects];
-}
-
-- (void)onKeyboardPressed:(UITextField *)textField {
- NSString* inputText = textField.text;
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- // If the text became empty, we know the user pressed the backspace key.
- if ([inputText isEqual:@""]) {
- LiSendKeyboardEvent(0x08, KEY_ACTION_DOWN, 0);
- usleep(50 * 1000);
- LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
- } else {
- // Character 0 will be our known sentinel value
-
- // Check if any characters exist which can't be represented in a basic key event
- for (int i = 1; i < [inputText length]; i++) {
- struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
- if (event.keycode == 0) {
- // We found an unknown key, so send the entire string as UTF-8
- const char* utf8String = [inputText UTF8String];
-
- // Skip the first character which is our sentinel
- LiSendUtf8TextEvent(utf8String + 1, (int)strlen(utf8String) - 1);
- return;
- }
- }
-
- // We didn't find any unknown characters, so send them all as basic key events
- for (int i = 1; i < [inputText length]; i++) {
- struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
- assert(event.keycode != 0);
- [self sendLowLevelEvent:event];
- }
- }
- });
-
- // Reset text field back to known state
- textField.text = @"0";
-
- // Move the insertion point back to the end of the text box
- UITextRange *textRange = [textField textRangeFromPosition:textField.endOfDocument toPosition:textField.endOfDocument];
- [textField setSelectedTextRange:textRange];
-}
-
-- (void)specialCharPressed:(UIKeyCommand *)cmd {
- struct KeyEvent event = [KeyboardSupport translateKeyEvent:0x20 withModifierFlags:[cmd modifierFlags]];
- event.keycode = [[dictCodes valueForKey:[cmd input]] intValue];
- [self sendLowLevelEvent:event];
-}
-
-- (void)keyPressed:(UIKeyCommand *)cmd {
- struct KeyEvent event = [KeyboardSupport translateKeyEvent:[[cmd input] characterAtIndex:0] withModifierFlags:[cmd modifierFlags]];
- [self sendLowLevelEvent:event];
-}
-
-- (void)sendLowLevelEvent:(struct KeyEvent)event {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- // When we want to send a modified key (like uppercase letters) we need to send the
- // modifier ("shift") seperately from the key itself.
- if (event.modifier != 0) {
- LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
- }
- // Let the host know these are not (necessarily) normalized to US English scancodes
- LiSendKeyboardEvent2(event.keycode, KEY_ACTION_DOWN, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
- usleep(50 * 1000);
- LiSendKeyboardEvent2(event.keycode, KEY_ACTION_UP, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
- if (event.modifier != 0) {
- LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
- }
- });
-}
-
-- (BOOL)canBecomeFirstResponder {
- return YES;
-}
-
-- (NSArray *)keyCommands
-{
- NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= ";
-
- NSMutableArray * commands = [NSMutableArray array];
- dictCodes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: 0x0d], @"\r", [NSNumber numberWithInt: 0x08], @"\b", [NSNumber numberWithInt: 0x1b], UIKeyInputEscape, [NSNumber numberWithInt: 0x28], UIKeyInputDownArrow, [NSNumber numberWithInt: 0x26], UIKeyInputUpArrow, [NSNumber numberWithInt: 0x25], UIKeyInputLeftArrow, [NSNumber numberWithInt: 0x27], UIKeyInputRightArrow, nil];
-
- [charset enumerateSubstringsInRange:NSMakeRange(0, charset.length)
- options:NSStringEnumerationByComposedCharacterSequences
- usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
- [commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:0 action:@selector(keyPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierShift action:@selector(keyPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierControl action:@selector(keyPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierAlternate action:@selector(keyPressed:)]];
- }];
-
- for (NSString *c in [dictCodes keyEnumerator]) {
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:0
- action:@selector(specialCharPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:UIKeyModifierShift
- action:@selector(specialCharPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:UIKeyModifierShift | UIKeyModifierAlternate
- action:@selector(specialCharPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:UIKeyModifierShift | UIKeyModifierControl
- action:@selector(specialCharPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:UIKeyModifierControl
- action:@selector(specialCharPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
- action:@selector(specialCharPressed:)]];
- [commands addObject:[UIKeyCommand keyCommandWithInput:c
- modifierFlags:UIKeyModifierAlternate
- action:@selector(specialCharPressed:)]];
- }
-
- return commands;
-}
-
-- (void)connectedStateDidChangeWithIdentifier:(NSUUID * _Nonnull)identifier isConnected:(BOOL)isConnected {
- NSLog(@"Citrix X1 mouse state change: %@ -> %s",
- identifier, isConnected ? "connected" : "disconnected");
-}
-
-- (void)mouseDidMoveWithIdentifier:(NSUUID * _Nonnull)identifier deltaX:(int16_t)deltaX deltaY:(int16_t)deltaY {
- accumulatedMouseDeltaX += deltaX / X1_MOUSE_SPEED_DIVISOR;
- accumulatedMouseDeltaY += deltaY / X1_MOUSE_SPEED_DIVISOR;
-
- short shortX = (short)accumulatedMouseDeltaX;
- short shortY = (short)accumulatedMouseDeltaY;
-
- if (shortX == 0 && shortY == 0) {
- return;
- }
-
- LiSendMouseMoveEvent(shortX, shortY);
-
- accumulatedMouseDeltaX -= shortX;
- accumulatedMouseDeltaY -= shortY;
-}
-
-- (int) buttonFromX1ButtonCode:(enum X1MouseButton)button {
- switch (button) {
- case X1MouseButtonLeft:
- return BUTTON_LEFT;
- case X1MouseButtonRight:
- return BUTTON_RIGHT;
- case X1MouseButtonMiddle:
- return BUTTON_MIDDLE;
- default:
- return -1;
- }
-}
-
-- (void)mouseDownWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button {
- LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, [self buttonFromX1ButtonCode:button]);
-}
-
-- (void)mouseUpWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button {
- LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, [self buttonFromX1ButtonCode:button]);
-}
-
-- (void)wheelDidScrollWithIdentifier:(NSUUID * _Nonnull)identifier deltaZ:(int8_t)deltaZ {
- LiSendScrollEvent(deltaZ);
-}
-
-#if !TARGET_OS_TV
-- (BOOL)isMultipleTouchEnabled {
- return YES;
-}
-#endif
@end
diff --git a/Limelight/Limelight-Info.plist b/Limelight/Limelight-Info.plist
index 963c433..0ea2105 100644
--- a/Limelight/Limelight-Info.plist
+++ b/Limelight/Limelight-Info.plist
@@ -2,10 +2,6 @@
- CADisableMinimumFrameDuration
-
- CADisableMinimumFrameDurationOnPhone
-
CFBundleDevelopmentRegion
en
CFBundleDisplayName
@@ -21,24 +17,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- $(MARKETING_VERSION)
+ 1.1.3
CFBundleSignature
????
CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- GCSupportedGameControllers
-
-
- ProfileName
- ExtendedGamepad
-
-
- GCSupportsControllerUserInteraction
-
- LSSupportsGameMode
-
- ITSAppUsesNonExemptEncryption
-
+ 1
LSApplicationCategoryType
public.app-category.games
LSRequiresIPhoneOS
@@ -48,18 +31,15 @@
NSAllowsArbitraryLoads
- NSBluetoothAlwaysUsageDescription
- Bluetooth access allows Moonlight to connect to Citrix X1 mice.
- NSBluetoothPeripheralUsageDescription
- Bluetooth access allows Moonlight to connect to Citrix X1 mice.
- NSBonjourServices
+ UIAppFonts
- _nvstream._tcp
+ Roboto-Black.ttf
+ Roboto-Bold.ttf
+ Roboto-Light.ttf
+ Roboto-Medium.ttf
+ Roboto-Regular.ttf
+ Roboto-Thin.ttf
- NSLocalNetworkUsageDescription
- Moonlight uses the local network to connect to your gaming PC for streaming.
- UIApplicationSupportsIndirectInputEvents
-
UILaunchStoryboardName
Launch Screen
UIMainStoryboardFile
diff --git a/Limelight/Limelight-Prefix.pch b/Limelight/Limelight-Prefix.pch
index 4e945b8..b6e3f7e 100644
--- a/Limelight/Limelight-Prefix.pch
+++ b/Limelight/Limelight-Prefix.pch
@@ -5,20 +5,14 @@
//
#import
-#include
#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#endif
#ifdef __OBJC__
-
-#if TARGET_OS_IPHONE
#import
-#elif TARGET_OS_MAC
- #import
-#endif
-#import
-#import
-#import "Logger.h"
-#endif
+ #import
+ #import
+ #import "Logger.h"
+#endif
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/.xccurrentversion b/Limelight/Limelight.xcdatamodeld/.xccurrentversion
index b202eb9..2346a99 100644
--- a/Limelight/Limelight.xcdatamodeld/.xccurrentversion
+++ b/Limelight/Limelight.xcdatamodeld/.xccurrentversion
@@ -3,6 +3,6 @@
_XCCurrentVersionName
- Moonlight v1.10.xcdatamodel
+ Moonlight v1.0-2.xcdatamodel
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.0-2.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.0-2.xcdatamodel/contents
index a9ff5b4..6844893 100644
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.0-2.xcdatamodel/contents
+++ b/Limelight/Limelight.xcdatamodeld/Moonlight v1.0-2.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
@@ -12,22 +12,21 @@
-
+
-
-
-
-
-
+
+
+
+
-
+
-
+
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.1.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.1.xcdatamodel/contents
deleted file mode 100644
index 090bd63..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.1.xcdatamodel/contents
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents
deleted file mode 100644
index a2cc61c..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.10.xcdatamodel/contents
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.2.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.2.xcdatamodel/contents
deleted file mode 100644
index 0d3feba..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.2.xcdatamodel/contents
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.3.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.3.xcdatamodel/contents
deleted file mode 100644
index 9eee4f1..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.3.xcdatamodel/contents
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.4.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.4.xcdatamodel/contents
deleted file mode 100644
index ce5077c..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.4.xcdatamodel/contents
+++ /dev/null
@@ -1,41 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.5.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.5.xcdatamodel/contents
deleted file mode 100644
index 89bac30..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.5.xcdatamodel/contents
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.6.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.6.xcdatamodel/contents
deleted file mode 100644
index 39de9e9..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.6.xcdatamodel/contents
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.7.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.7.xcdatamodel/contents
deleted file mode 100644
index 712c76a..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.7.xcdatamodel/contents
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.8.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.8.xcdatamodel/contents
deleted file mode 100644
index 19d6d78..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.8.xcdatamodel/contents
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Limelight.xcdatamodeld/Moonlight v1.9.xcdatamodel/contents b/Limelight/Limelight.xcdatamodeld/Moonlight v1.9.xcdatamodel/contents
deleted file mode 100644
index 30a96bc..0000000
--- a/Limelight/Limelight.xcdatamodeld/Moonlight v1.9.xcdatamodel/contents
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Limelight/Network/AppAssetManager.h b/Limelight/Network/AppAssetManager.h
index d202072..42f7720 100644
--- a/Limelight/Network/AppAssetManager.h
+++ b/Limelight/Network/AppAssetManager.h
@@ -6,6 +6,7 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "TemporaryApp.h"
#import "HttpManager.h"
#import "TemporaryHost.h"
@@ -21,6 +22,5 @@
- (id) initWithCallback:(id)callback;
- (void) retrieveAssetsFromHost:(TemporaryHost*)host;
- (void) stopRetrieving;
-+ (NSString*) boxArtPathForApp:(TemporaryApp*)app;
@end
diff --git a/Limelight/Network/AppAssetManager.m b/Limelight/Network/AppAssetManager.m
index 4a6480d..f8a8f5d 100644
--- a/Limelight/Network/AppAssetManager.m
+++ b/Limelight/Network/AppAssetManager.m
@@ -19,22 +19,6 @@
static const int MAX_REQUEST_COUNT = 4;
-+ (NSString*) boxArtPathForApp:(TemporaryApp*)app {
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
- NSString *filePath = [paths objectAtIndex:0];
-
- // Keep app assets separate by host UUID
- filePath = [filePath stringByAppendingPathComponent:app.host.uuid];
-
- // Use the app ID as the file name
- filePath = [filePath stringByAppendingPathComponent:app.id];
-
- // Add a png extension
- filePath = [filePath stringByAppendingPathExtension:@"png"];
-
- return filePath;
-}
-
- (id) initWithCallback:(id)callback {
self = [super init];
_callback = callback;
@@ -46,7 +30,7 @@ static const int MAX_REQUEST_COUNT = 4;
- (void) retrieveAssetsFromHost:(TemporaryHost*)host {
for (TemporaryApp* app in host.appList) {
- if (![[NSFileManager defaultManager] fileExistsAtPath:[AppAssetManager boxArtPathForApp:app]]) {
+ if (app.image == nil) {
AppAssetRetriever* retriever = [[AppAssetRetriever alloc] init];
retriever.app = app;
retriever.host = host;
diff --git a/Limelight/Network/AppAssetResponse.h b/Limelight/Network/AppAssetResponse.h
index ad225f8..4e9ac7b 100644
--- a/Limelight/Network/AppAssetResponse.h
+++ b/Limelight/Network/AppAssetResponse.h
@@ -6,6 +6,7 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "HttpResponse.h"
@interface AppAssetResponse : NSObject
diff --git a/Limelight/Network/AppAssetResponse.m b/Limelight/Network/AppAssetResponse.m
index 0fb9ff8..9a41e64 100644
--- a/Limelight/Network/AppAssetResponse.m
+++ b/Limelight/Network/AppAssetResponse.m
@@ -16,8 +16,10 @@
self.statusMessage = @"App asset has no status message";
self.statusCode = -1;
}
+
- (UIImage*) getImage {
- return [[UIImage alloc] initWithData:self.data];
+ UIImage* appImage = [[UIImage alloc] initWithData:self.data];
+ return appImage;
}
@end
diff --git a/Limelight/Network/AppAssetRetriever.h b/Limelight/Network/AppAssetRetriever.h
index 07ee687..b897a0f 100644
--- a/Limelight/Network/AppAssetRetriever.h
+++ b/Limelight/Network/AppAssetRetriever.h
@@ -6,6 +6,7 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "TemporaryHost.h"
#import "TemporaryApp.h"
#import "AppAssetManager.h"
diff --git a/Limelight/Network/AppAssetRetriever.m b/Limelight/Network/AppAssetRetriever.m
index 75580db..824b423 100644
--- a/Limelight/Network/AppAssetRetriever.m
+++ b/Limelight/Network/AppAssetRetriever.m
@@ -18,20 +18,18 @@ static const double RETRY_DELAY = 2; // seconds
static const int MAX_ATTEMPTS = 5;
- (void) main {
+ UIImage* appImage = nil;
int attempts = 0;
- while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) {
- HttpManager* hMan = [[HttpManager alloc] initWithHost:_host];
+ while (![self isCancelled] && appImage == nil && attempts++ < MAX_ATTEMPTS) {
+
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] deviceName:deviceName cert:[CryptoManager readCertFromFile]];
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
-
- if (appAssetResp.data != nil) {
- NSString* boxArtPath = [AppAssetManager boxArtPathForApp:self.app];
- [[NSFileManager defaultManager] createDirectoryAtPath:[boxArtPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
- [appAssetResp.data writeToFile:boxArtPath atomically:NO];
- break;
- }
- if (![self isCancelled]) {
+ appImage = [UIImage imageWithData:appAssetResp.data];
+ self.app.image = UIImagePNGRepresentation(appImage);
+
+ if (![self isCancelled] && appImage == nil) {
[NSThread sleepForTimeInterval:RETRY_DELAY];
}
}
diff --git a/Limelight/Network/AppListResponse.m b/Limelight/Network/AppListResponse.m
index d5d3010..ab6bc22 100644
--- a/Limelight/Network/AppListResponse.m
+++ b/Limelight/Network/AppListResponse.m
@@ -19,8 +19,6 @@
static const char* TAG_APP = "App";
static const char* TAG_APP_TITLE = "AppTitle";
static const char* TAG_APP_ID = "ID";
-static const char* TAG_HDR_SUPPORTED = "IsHdrSupported";
-static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
- (void)populateWithData:(NSData *)xml {
self.data = xml;
@@ -44,7 +42,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
xmlChar* statusStr = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_CODE UTF8String]);
if (statusStr != NULL) {
- int status = (int)[[NSString stringWithUTF8String:(const char*)statusStr] longLongValue];
+ int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
xmlFree(statusStr);
self.statusCode = status;
}
@@ -68,14 +66,14 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
xmlNodePtr appInfoNode = node->xmlChildrenNode;
NSString* appName = @"";
NSString* appId = nil;
- NSString* hdrSupported = @"0";
- NSString* appInstallPath = nil;
while (appInfoNode != NULL) {
if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_TITLE)) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
if (nodeVal != NULL) {
appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
+ } else {
+ appName = @"";
}
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_ID)) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
@@ -83,28 +81,13 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
}
- } else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_HDR_SUPPORTED)) {
- xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
- if (nodeVal != NULL) {
- hdrSupported = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
- xmlFree(nodeVal);
- }
- } else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_INSTALL_PATH)) {
- xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
- if (nodeVal != NULL) {
- appInstallPath = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
- xmlFree(nodeVal);
- }
}
-
appInfoNode = appInfoNode->next;
}
if (appId != nil) {
TemporaryApp* app = [[TemporaryApp alloc] init];
app.name = appName;
app.id = appId;
- app.hdrSupported = [hdrSupported intValue] != 0;
- app.installPath = appInstallPath;
[_appList addObject:app];
}
}
@@ -112,42 +95,6 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
}
xmlFreeDoc(docPtr);
-
-#ifdef ENABLE_APP_STORE_RESTRICTIONS
- // APP STORE REVIEW COMPLIANCE
- //
- // Remove default Steam entry from the app list to comply with Apple App Store Guideline 4.2.7d:
- //
- // The UI appearing on the client does not resemble an iOS or App Store view, does not provide a store-like interface,
- // or include the ability to browse, select, or purchase software not already owned or licensed by the user.
- //
- // However, if the user manually adds Steam themselves, then we will display it.
- TemporaryApp* officialSteamApp = nil;
- TemporaryApp* manuallyAddedSteamApp = nil;
- for (TemporaryApp* app in _appList) {
- if (app.installPath != nil && [[app.installPath lowercaseString] hasSuffix:@"\\steam\\"]) {
- // The official Steam app is marked as HDR supported, while manually added ones are not.
- if ([app.name isEqualToString:@"Steam"] && app.hdrSupported) {
- officialSteamApp = app;
- }
- else {
- manuallyAddedSteamApp = app;
- }
- }
- }
-
- // To be safe, don't do anything if we didn't find an HDR-enabled Steam app.
- if (officialSteamApp != nil) {
- // If we didn't find a manually added Steam app, remove the official one to
- // comply with the App Store guidelines.
- if (manuallyAddedSteamApp == nil) {
- [_appList removeObject:officialSteamApp];
- }
- else {
- [_appList removeObject:manuallyAddedSteamApp];
- }
- }
-#endif
}
- (NSSet*) getAppList {
diff --git a/Limelight/Network/ConnectionHelper.h b/Limelight/Network/ConnectionHelper.h
deleted file mode 100644
index cd88599..0000000
--- a/Limelight/Network/ConnectionHelper.h
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// ConnectionHelper.h
-// Moonlight macOS
-//
-// Created by Felix Kratz on 22.03.18.
-// Copyright © 2018 Felix Kratz. All rights reserved.
-//
-
-#import "AppListResponse.h"
-#import "TemporaryHost.h"
-
-#ifndef ConnectionHelper_h
-#define ConnectionHelper_h
-
-@interface ConnectionHelper : NSObject
-
-+(AppListResponse*) getAppListForHost:(TemporaryHost*)host;
-
-@end
-
-#endif /* ConnectionHelper_h */
diff --git a/Limelight/Network/ConnectionHelper.m b/Limelight/Network/ConnectionHelper.m
deleted file mode 100644
index f312427..0000000
--- a/Limelight/Network/ConnectionHelper.m
+++ /dev/null
@@ -1,41 +0,0 @@
-//
-// ConnectionHelper.m
-// Moonlight macOS
-//
-// Created by Felix Kratz on 22.03.18.
-// Copyright © 2018 Felix Kratz. All rights reserved.
-//
-
-#import "ConnectionHelper.h"
-#import "ServerInfoResponse.h"
-#import "HttpManager.h"
-#import "PairManager.h"
-#import "DiscoveryManager.h"
-
-@implementation ConnectionHelper
-
-+(AppListResponse*) getAppListForHost:(TemporaryHost*)host {
- HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
-
- // Try up to 5 times to get the app list
- AppListResponse* appListResp = nil;
- for (int i = 0; i < 5; i++) {
- appListResp = [[AppListResponse alloc] init];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:appListResp withUrlRequest:[hMan newAppListRequest]]];
- if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
- Log(LOG_W, @"Failed to get applist on try %d: %@", i, appListResp.statusMessage);
-
- // Wait for one second then retry
- [NSThread sleepForTimeInterval:1];
- }
- else {
- Log(LOG_I, @"App list successfully retreived - took %d tries", i);
- break;
- }
- }
-
- return appListResp;
-}
-
-@end
-
diff --git a/Limelight/Network/DiscoveryManager.h b/Limelight/Network/DiscoveryManager.h
index ba26549..7d5dfcc 100644
--- a/Limelight/Network/DiscoveryManager.h
+++ b/Limelight/Network/DiscoveryManager.h
@@ -6,6 +6,7 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "MDNSManager.h"
#import "TemporaryHost.h"
@@ -21,11 +22,8 @@
- (void) startDiscovery;
- (void) stopDiscovery;
- (void) stopDiscoveryBlocking;
-- (void) resetDiscoveryState;
- (BOOL) addHostToDiscovery:(TemporaryHost*)host;
- (void) removeHostFromDiscovery:(TemporaryHost*)host;
-- (void) pauseDiscoveryForHost:(TemporaryHost *)host;
-- (void) resumeDiscoveryForHost:(TemporaryHost *)host;
- (void) discoverHost:(NSString*)hostAddress withCallback:(void (^)(TemporaryHost*, NSString*))callback;
@end
diff --git a/Limelight/Network/DiscoveryManager.m b/Limelight/Network/DiscoveryManager.m
index a72df63..24dccfc 100644
--- a/Limelight/Network/DiscoveryManager.m
+++ b/Limelight/Network/DiscoveryManager.m
@@ -15,14 +15,8 @@
#import "ServerInfoResponse.h"
#import "IdManager.h"
-#include
-#include
-#include
-#include
-
@implementation DiscoveryManager {
NSMutableArray* _hostQueue;
- NSMutableSet* _pausedHosts;
id _callback;
MDNSManager* _mdnsMan;
NSOperationQueue* _opQueue;
@@ -39,7 +33,6 @@
_callback = callback;
shouldDiscover = NO;
_hostQueue = [NSMutableArray array];
- _pausedHosts = [NSMutableSet set];
for (TemporaryHost* host in hosts)
{
[self addHostToDiscovery:host];
@@ -48,194 +41,44 @@
_opQueue = [[NSOperationQueue alloc] init];
_mdnsMan = [[MDNSManager alloc] initWithCallback:self];
- [CryptoManager generateKeyPairUsingSSL];
+ [CryptoManager generateKeyPairUsingSSl];
_uniqueId = [IdManager getUniqueId];
_cert = [CryptoManager readCertFromFile];
return self;
}
-+ (BOOL) isAddressLAN:(in_addr_t)addr {
- addr = htonl(addr);
-
- // 10.0.0.0/8
- if ((addr & 0xFF000000) == 0x0A000000) {
- return YES;
- }
- // 172.16.0.0/12
- else if ((addr & 0xFFF00000) == 0xAC100000) {
- return YES;
- }
- // 192.168.0.0/16
- else if ((addr & 0xFFFF0000) == 0xC0A80000) {
- return YES;
- }
- // 169.254.0.0/16
- else if ((addr & 0xFFFF0000) == 0xA9FE0000) {
- return YES;
- }
- // 100.64.0.0/10 - RFC6598 official CGN address (shouldn't see this in a LAN)
- else if ((addr & 0xFFC00000) == 0x64400000) {
- return YES;
- }
-
- return NO;
-}
-
-// This ensures that only RFC 1918 IPv4 addresses can be passed to
-// the Add PC dialog. This is required to comply with Apple App Store
-// Guideline 4.2.7a.
-+ (BOOL) isProhibitedAddress:(NSString*)address {
-#ifdef ENABLE_APP_STORE_RESTRICTIONS
- struct addrinfo hints;
- struct addrinfo* result;
- int err;
-
- // We're explicitly using AF_INET here because we don't want to
- // ever receive a synthesized IPv6 address here, even on NAT64.
- // IPv6 addresses are not restricted here because we cannot easily
- // tell whether they are local or not.
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_INET;
- err = getaddrinfo([address UTF8String], NULL, &hints, &result);
- if (err != 0 || result == NULL) {
- Log(LOG_W, @"getaddrinfo(%@) failed: %d", address, err);
- return NO;
- }
-
- if (result->ai_family != AF_INET) {
- // This should never happen due to our hints
- assert(result->ai_family == AF_INET);
- Log(LOG_W, @"Unexpected address family: %d", result->ai_family);
- freeaddrinfo(result);
- return NO;
- }
-
- BOOL ret = ![DiscoveryManager isAddressLAN:((struct sockaddr_in*)result->ai_addr)->sin_addr.s_addr];
- freeaddrinfo(result);
-
- return ret;
-#else
- return NO;
-#endif
-}
-
-- (ServerInfoResponse*) getServerInfoResponseForAddress:(NSString*)address {
- HttpManager* hMan = [[HttpManager alloc] initWithAddress:address httpsPort:0 serverCert:nil];
- ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
- return serverInfoResponse;
-}
-
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
- BOOL prohibitedAddress = [DiscoveryManager isProhibitedAddress:hostAddress];
- NSString* prohibitedAddressMessage = [NSString stringWithFormat: @"Moonlight only supports adding PCs on your local network on %s.",
- #if TARGET_OS_TV
- "tvOS"
- #else
- "iOS"
- #endif
- ];
- ServerInfoResponse* serverInfoResponse = [self getServerInfoResponseForAddress:hostAddress];
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId deviceName:deviceName cert:_cert];
+ ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
TemporaryHost* host = nil;
if ([serverInfoResponse isStatusOk]) {
host = [[TemporaryHost alloc] init];
host.activeAddress = host.address = hostAddress;
- host.state = StateOnline;
+ host.online = YES;
[serverInfoResponse populateHost:host];
-
- // Check if this is a new PC
- if (![self getHostInDiscovery:host.uuid]) {
- // Enforce LAN restriction for App Store Guideline 4.2.7a
- if ([DiscoveryManager isProhibitedAddress:hostAddress]) {
- // We have a prohibited address. This might be because the user specified their WAN address
- // instead of their LAN address. If that's the case, we'll try their LAN address and if we
- // can reach it through that address, we'll allow it.
- ServerInfoResponse* lanInfo = [self getServerInfoResponseForAddress:host.localAddress];
- if ([lanInfo isStatusOk]) {
- TemporaryHost* lanHost = [[TemporaryHost alloc] init];
- [lanInfo populateHost:lanHost];
-
- if (![lanHost.uuid isEqualToString:host.uuid]) {
- // This is a different host, so it's prohibited
- prohibitedAddress = YES;
- }
- else {
- // This is the same host that is reachable on the LAN
- prohibitedAddress = NO;
- }
- }
- else {
- // LAN request failed, so it's a prohibited address
- prohibitedAddress = YES;
- }
- }
- else {
- // It's an RFC 1918 IPv4 address or IPv6 address which counts as LAN
- prohibitedAddress = NO;
- }
-
- if (prohibitedAddress) {
- callback(nil, prohibitedAddressMessage);
- return;
- }
- else if ([DiscoveryManager isAddressLAN:inet_addr([hostAddress UTF8String])]) {
- // Don't send a STUN request if we're connected to a VPN. We'll likely get the VPN
- // gateway's external address rather than the external address of the LAN.
- if (![Utils isActiveNetworkVPN]) {
- // This host was discovered over a permissible LAN address, so we can update our
- // external address for this host.
- struct in_addr wanAddr;
- int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
- if (err == 0) {
- char addrStr[INET_ADDRSTRLEN];
- inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
- host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
- }
- }
- }
- }
-
if (![self addHostToDiscovery:host]) {
- callback(nil, @"Host information updated");
+ callback(nil, @"Host already added");
} else {
callback(host, nil);
}
- } else if (!prohibitedAddress) {
- callback(nil, @"Could not connect to host.\n\nIf you're hosting using GeForce Experience, make sure you've enabled the toggle on the SHIELD tab.\n\nIf you're hosting using Sunshine, ensure it is running properly. If you're using a non-default port, you will need to include that here.");
} else {
- callback(nil, prohibitedAddressMessage);
+ callback(nil, @"Could not connect to host. Ensure GameStream is enabled in GeForce Experience on your PC.");
}
-}
-
-- (void) resetDiscoveryState {
- // Allow us to rediscover hosts that were already found before
- [_mdnsMan forgetHosts];
+
}
- (void) startDiscovery {
- if (shouldDiscover) {
- return;
- }
-
Log(LOG_I, @"Starting discovery");
shouldDiscover = YES;
[_mdnsMan searchForHosts];
-
- @synchronized (_hostQueue) {
- for (TemporaryHost* host in _hostQueue) {
- if (![_pausedHosts containsObject:host]) {
- [_opQueue addOperation:[self createWorkerForHost:host]];
- }
- }
+ for (TemporaryHost* host in _hostQueue) {
+ [_opQueue addOperation:[self createWorkerForHost:host]];
}
}
- (void) stopDiscovery {
- if (!shouldDiscover) {
- return;
- }
-
Log(LOG_I, @"Stopping discovery");
shouldDiscover = NO;
[_mdnsMan stopSearching];
@@ -244,18 +87,10 @@
- (void) stopDiscoveryBlocking {
Log(LOG_I, @"Stopping discovery and waiting for workers to stop");
-
- if (shouldDiscover) {
- shouldDiscover = NO;
- [_mdnsMan stopSearching];
- [_opQueue cancelAllOperations];
- }
-
- // Ensure we always wait, just in case discovery
- // was stopped already but in an async manner that
- // left operations in progress.
+ shouldDiscover = NO;
+ [_mdnsMan stopSearching];
+ [_opQueue cancelAllOperations];
[_opQueue waitUntilAllOperationsAreFinished];
-
Log(LOG_I, @"All discovery workers stopped");
}
@@ -266,109 +101,59 @@
TemporaryHost *existingHost = [self getHostInDiscovery:host.uuid];
if (existingHost != nil) {
- // NB: Our logic here depends on the fact that we never propagate
- // the entire TemporaryHost to existingHost. In particular, when mDNS
- // discovers a PC and we poll it, we will do so over HTTP which will
- // not have accurate pair state. The fields explicitly copied below
- // are accurate though.
-
// Update address of existing host
- if (host.address != nil) {
- existingHost.address = host.address;
- }
- if (host.localAddress != nil) {
- existingHost.localAddress = host.localAddress;
- }
- if (host.ipv6Address != nil) {
- existingHost.ipv6Address = host.ipv6Address;
- }
- if (host.externalAddress != nil) {
- existingHost.externalAddress = host.externalAddress;
- }
- existingHost.activeAddress = host.activeAddress;
- existingHost.state = host.state;
+ existingHost.address = existingHost.activeAddress = host.address;
return NO;
}
else {
- @synchronized (_hostQueue) {
- [_hostQueue addObject:host];
- if (shouldDiscover) {
- [_opQueue addOperation:[self createWorkerForHost:host]];
- }
+ [_hostQueue addObject:host];
+ if (shouldDiscover) {
+ [_opQueue addOperation:[self createWorkerForHost:host]];
}
return YES;
}
}
- (void) removeHostFromDiscovery:(TemporaryHost *)host {
- @synchronized (_hostQueue) {
- for (DiscoveryWorker* worker in [_opQueue operations]) {
- if ([worker getHost] == host) {
- [worker cancel];
+ for (DiscoveryWorker* worker in [_opQueue operations]) {
+ if ([worker getHost] == host) {
+ [worker cancel];
+ }
+ }
+ [_hostQueue removeObject:host];
+}
+
+// Override from MDNSCallback
+- (void)updateHosts:(NSArray *)hosts {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ // Discover the hosts before adding to eliminate duplicates
+ for (TemporaryHost* host in hosts) {
+ Log(LOG_I, @"Found host through MDNS: %@:", host.name);
+ // Since this is on a background thread, we do not need to use the opQueue
+ DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
+ [worker discoverHost];
+ if ([self addHostToDiscovery:host]) {
+ Log(LOG_I, @"Adding host to discovery: %@", host.name);
+ [_callback updateAllHosts:_hostQueue];
+ } else {
+ Log(LOG_I, @"Not adding host to discovery: %@", host.name);
}
}
-
- [_hostQueue removeObject:host];
- [_pausedHosts removeObject:host];
- }
-}
-
-- (void) pauseDiscoveryForHost:(TemporaryHost *)host {
- @synchronized (_hostQueue) {
- // Stop any worker for the host
- for (DiscoveryWorker* worker in [_opQueue operations]) {
- if ([worker getHost] == host) {
- [worker cancel];
- }
- }
-
- // Add it to the paused hosts list
- [_pausedHosts addObject:host];
- }
-}
-
-- (void) resumeDiscoveryForHost:(TemporaryHost *)host {
- @synchronized (_hostQueue) {
- // Remove it from the paused hosts list
- [_pausedHosts removeObject:host];
-
- // Start discovery again
- if (shouldDiscover) {
- [_opQueue addOperation:[self createWorkerForHost:host]];
- }
- }
-}
-
-// Override from MDNSCallback - called in a worker thread
-- (void)updateHost:(TemporaryHost*)host {
- // Discover the hosts before adding to eliminate duplicates
- Log(LOG_D, @"Found host through MDNS: %@:", host.name);
- // Since this is on a background thread, we do not need to use the opQueue
- DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
- [worker discoverHost];
- if ([self addHostToDiscovery:host]) {
- Log(LOG_I, @"Found new host through MDNS: %@:", host.name);
- @synchronized (_hostQueue) {
- [_callback updateAllHosts:_hostQueue];
- }
- } else {
- Log(LOG_D, @"Found existing host through MDNS: %@", host.name);
- }
+ });
}
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
- @synchronized (_hostQueue) {
- for (TemporaryHost* discoveredHost in _hostQueue) {
- if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
- return discoveredHost;
- }
+ for (int i = 0; i < _hostQueue.count; i++) {
+ TemporaryHost* discoveredHost = [_hostQueue objectAtIndex:i];
+ if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
+ return discoveredHost;
}
}
return nil;
}
- (NSOperation*) createWorkerForHost:(TemporaryHost*)host {
- DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId];
+ DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId cert:_cert];
return worker;
}
diff --git a/Limelight/Network/DiscoveryWorker.h b/Limelight/Network/DiscoveryWorker.h
index da6491d..c8d2a6c 100644
--- a/Limelight/Network/DiscoveryWorker.h
+++ b/Limelight/Network/DiscoveryWorker.h
@@ -6,11 +6,12 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "TemporaryHost.h"
@interface DiscoveryWorker : NSOperation
-- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId;
+- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert;
- (void) discoverHost;
- (TemporaryHost*) getHost;
diff --git a/Limelight/Network/DiscoveryWorker.m b/Limelight/Network/DiscoveryWorker.m
index a3c80aa..15ae3b4 100644
--- a/Limelight/Network/DiscoveryWorker.m
+++ b/Limelight/Network/DiscoveryWorker.m
@@ -16,14 +16,16 @@
@implementation DiscoveryWorker {
TemporaryHost* _host;
NSString* _uniqueId;
+ NSData* _cert;
}
static const float POLL_RATE = 2.0f; // Poll every 2 seconds
-- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId {
+- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert {
self = [super init];
_host = host;
_uniqueId = uniqueId;
+ _cert = cert;
return self;
}
@@ -46,14 +48,11 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
if (_host.localAddress != nil) {
[array addObject:_host.localAddress];
}
- if (_host.address != nil) {
- [array addObject:_host.address];
- }
if (_host.externalAddress != nil) {
[array addObject:_host.externalAddress];
}
- if (_host.ipv6Address != nil) {
- [array addObject:_host.ipv6Address];
+ if (_host.address != nil) {
+ [array addObject:_host.address];
}
// Remove duplicate addresses from the list.
@@ -89,9 +88,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
Log(LOG_D, @"%@ has %d unique addresses", _host.name, [addresses count]);
- // Give the PC 2 tries to respond before declaring it offline if we've seen it before.
- // If this is an unknown PC, update the status after 1 attempt to get the UI refreshed quickly.
- for (int i = 0; i < (_host.state == StateUnknown ? 1 : 2); i++) {
+ // Give the PC 3 tries to respond before declaring it offline
+ for (int i = 0; i < 3; i++) {
for (NSString *address in addresses) {
if (self.cancelled) {
// Get out without updating the status because
@@ -100,11 +98,11 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
return;
}
- ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
+ ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address];
receivedResponse = [self checkResponse:serverInfoResp];
if (receivedResponse) {
- _host.activeAddress = address;
[serverInfoResp populateHost:_host];
+ _host.activeAddress = address;
// Update the database using the response
DataManager *dataManager = [[DataManager alloc] init];
@@ -114,22 +112,29 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
}
if (receivedResponse) {
- Log(LOG_D, @"Received serverinfo response on try %d", i);
+ Log(LOG_I, @"Received serverinfo response on try %d", i);
break;
}
+ else {
+ // Wait for one second then retry
+ [NSThread sleepForTimeInterval:1];
+ }
}
- _host.state = receivedResponse ? StateOnline : StateOffline;
+ _host.online = receivedResponse;
if (receivedResponse) {
- Log(LOG_D, @"Received response from: %@\n{\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, @"Received response from: %@\n{\n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \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.uuid, _host.mac, _host.pairState, _host.online, _host.activeAddress);
}
}
-- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address cert:(NSData*)cert {
- HttpManager* hMan = [[HttpManager alloc] initWithAddress:address httpsPort:0 serverCert:cert];
+- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address {
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:address
+ uniqueId:_uniqueId
+ deviceName:deviceName
+ cert:_cert];
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
- withUrlRequest:[hMan newServerInfoRequest:true]
+ withUrlRequest:[hMan newServerInfoRequest]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
return response;
}
diff --git a/Limelight/Network/HttpManager.h b/Limelight/Network/HttpManager.h
index 4d23846..97caaaa 100644
--- a/Limelight/Network/HttpManager.h
+++ b/Limelight/Network/HttpManager.h
@@ -6,27 +6,24 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "HttpResponse.h"
#import "HttpRequest.h"
-#import "StreamConfiguration.h"
-#import "TemporaryHost.h"
@interface HttpManager : NSObject
-- (id) initWithHost:(TemporaryHost*) host;
-- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short) httpsPort serverCert:(NSData*) serverCert;
-- (void) setServerCert:(NSData*) serverCert;
-- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
+- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert;
+- (NSURLRequest*) newPairRequest:(NSData*)salt;
- (NSURLRequest*) newUnpairRequest;
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge;
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp;
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret;
- (NSURLRequest*) newPairChallenge;
- (NSURLRequest*) newAppListRequest;
-- (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
-- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
+- (NSURLRequest*) newServerInfoRequest;
- (NSURLRequest*) newHttpServerInfoRequest;
-- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config;
+- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid;
+- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId;
- (NSURLRequest*) newQuitAppRequest;
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
- (void) executeRequestSynchronously:(HttpRequest*)request;
diff --git a/Limelight/Network/HttpManager.m b/Limelight/Network/HttpManager.m
index 39bd354..6bef638 100644
--- a/Limelight/Network/HttpManager.m
+++ b/Limelight/Network/HttpManager.m
@@ -10,29 +10,28 @@
#import "HttpRequest.h"
#import "CryptoManager.h"
#import "TemporaryApp.h"
-#import "ServerInfoResponse.h"
#include
#include
-#include
-
-#define SHORT_TIMEOUT_SEC 2
-#define NORMAL_TIMEOUT_SEC 5
-#define LONG_TIMEOUT_SEC 60
-#define EXTRA_LONG_TIMEOUT_SEC 180
-
@implementation HttpManager {
- NSString* _urlSafeHostName;
+ NSURLSession* _urlSession;
NSString* _baseHTTPURL;
+ NSString* _baseHTTPSURL;
+ NSString* _host;
NSString* _uniqueId;
NSString* _deviceName;
- NSData* _serverCert;
+ NSData* _cert;
+ NSMutableData* _respData;
+ NSData* _requestResp;
+ dispatch_semaphore_t _requestLock;
- TemporaryHost *_host; // May be nil
- NSString* _baseHTTPSURL;
+ BOOL _errorOccurred;
}
+static const NSString* HTTP_PORT = @"47989";
+static const NSString* HTTPS_PORT = @"47984";
+
+ (NSData*) fixXmlVersion:(NSData*) xmlData {
NSString* dataString = [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding];
NSString* xmlString = [dataString stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [dataString length])];
@@ -40,115 +39,50 @@
return [xmlString dataUsingEncoding:NSUTF8StringEncoding];
}
-- (void) setServerCert:(NSData*) serverCert {
- _serverCert = serverCert;
-}
-
-- (id) initWithHost:(TemporaryHost*) host {
- self = [self initWithAddress:host.activeAddress httpsPort:host.httpsPort serverCert:host.serverCert];
- _host = host;
- return self;
-}
-
-- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short)httpsPort serverCert:(NSData*) serverCert {
+- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert {
self = [super init];
- // Use the same UID for all Moonlight clients to allow them
- // quit games started on another Moonlight client.
- _uniqueId = @"0123456789ABCDEF";
+ _host = host;
+ _uniqueId = uniqueId;
_deviceName = deviceName;
- _serverCert = serverCert;
-
- NSString* address = [Utils addressPortStringToAddress:hostAddressPortString];
- unsigned short port = [Utils addressPortStringToPort:hostAddressPortString];
-
- // If this is an IPv6 literal, we must properly enclose it in brackets
- if ([address containsString:@":"]) {
- _urlSafeHostName = [NSString stringWithFormat:@"[%@]", address];
- } else {
- _urlSafeHostName = address;
- }
-
- _baseHTTPURL = [NSString stringWithFormat:@"http://%@:%u", _urlSafeHostName, port];
-
- if (httpsPort) {
- _baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, httpsPort];
- }
-
+ _cert = cert;
+ _baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", host, HTTP_PORT];
+ _baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", host, HTTPS_PORT];
+ _requestLock = dispatch_semaphore_create(0);
+ _respData = [[NSMutableData alloc] init];
+ NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
+ _urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
return self;
}
-- (BOOL) ensureHttpsUrlPopulated:(bool)fastFail {
- if (!_baseHTTPSURL) {
- // Use the caller's provided port if one was specified
- if (_host && _host.httpsPort != 0) {
- _baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, _host.httpsPort];
- }
- else {
- // Query the host to retrieve the HTTPS port
- ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
- [self executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[self newHttpServerInfoRequest:false]]];
- TemporaryHost* dummyHost = [[TemporaryHost alloc] init];
- if (![serverInfoResponse isStatusOk]) {
- return NO;
- }
- [serverInfoResponse populateHost:dummyHost];
-
- // Pass the port back if the caller provided storage for it
- if (_host) {
- _host.httpsPort = dummyHost.httpsPort;
- }
-
- _baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, dummyHost.httpsPort];
- }
- }
-
- return YES;
-}
-
- (void) executeRequestSynchronously:(HttpRequest*)request {
- // This is a special case to handle failure of HTTPS port fetching
- if (!request.request) {
- if (request.response) {
- request.response.statusCode = EHOSTDOWN;
- request.response.statusMessage = @"Host is unreachable";
- }
-
- return;
- }
-
- __block NSData* requestResp = nil;
- __block NSError* respError = nil;
- __block dispatch_semaphore_t requestLock = dispatch_semaphore_create(0);
-
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) {
+ [_respData setLength:0];
+ [[_urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
if (error != NULL) {
Log(LOG_D, @"Connection error: %@", error);
- respError = error;
+ _errorOccurred = true;
}
else {
Log(LOG_D, @"Received response: %@", response);
if (data != NULL) {
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
- if ([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] != nil) {
- requestResp = [HttpManager fixXmlVersion:data];
+ [_respData appendData:data];
+ if ([[NSString alloc] initWithData:_respData encoding:NSUTF8StringEncoding] != nil) {
+ _requestResp = [HttpManager fixXmlVersion:_respData];
} else {
- requestResp = data;
+ _requestResp = _respData;
}
}
}
- dispatch_semaphore_signal(requestLock);
+ dispatch_semaphore_signal(_requestLock);
}] resume];
+ dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
- dispatch_semaphore_wait(requestLock, DISPATCH_TIME_FOREVER);
- [urlSession invalidateAndCancel];
-
- if (!respError && request.response) {
- [request.response populateWithData:requestResp];
+ if (!_errorOccurred && request.response) {
+ [request.response populateWithData:_requestResp];
// If the fallback error code was detected, issue the fallback request
if (request.response.statusCode == request.fallbackError && request.fallbackRequest != NULL) {
@@ -159,149 +93,92 @@
[self executeRequestSynchronously:request];
}
}
- else if (respError && [respError code] == NSURLErrorServerCertificateUntrusted) {
- // We must have a pinned cert for HTTPS. If we fail, it must be due to
- // a non-matching cert, not because we had no cert at all.
- assert(_serverCert != nil);
-
- if (request.fallbackRequest) {
- // This will fall back to HTTP on serverinfo queries to allow us to pair again
- // and get the server cert updated.
- Log(LOG_D, @"Attempting fallback request after certificate trust failure");
- request.request = request.fallbackRequest;
- request.fallbackError = 0;
- request.fallbackRequest = NULL;
- [self executeRequestSynchronously:request];
- }
- }
- else if (respError && request.response) {
- request.response.statusCode = [respError code];
- request.response.statusMessage = [respError localizedDescription];
- }
+ _errorOccurred = false;
}
-- (NSURLRequest*) createRequestFromString:(NSString*) urlString timeout:(int)timeout {
+- (NSURLRequest*) createRequestFromString:(NSString*) urlString enableTimeout:(BOOL)normalTimeout {
NSURL* url = [[NSURL alloc] initWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
- [request setTimeoutInterval:timeout];
+ if (normalTimeout) {
+ // Timeout the request after 7 seconds
+ [request setTimeoutInterval:7];
+ }
+ else {
+ // Timeout the request after 60 seconds
+ [request setTimeoutInterval:60];
+ }
return request;
}
-- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert {
+- (NSURLRequest*) newPairRequest:(NSData*)salt {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=getservercert&salt=%@&clientcert=%@",
- _baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:salt], [self bytesToHex:clientCert]];
+ _baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:salt], [self bytesToHex:_cert]];
// This call blocks while waiting for the user to input the PIN on the PC
- return [self createRequestFromString:urlString timeout:EXTRA_LONG_TIMEOUT_SEC];
+ return [self createRequestFromString:urlString enableTimeout:FALSE];
}
- (NSURLRequest*) newUnpairRequest {
- NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPURL, _uniqueId];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPSURL, _uniqueId];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientchallenge=%@",
- _baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ _baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&serverchallengeresp=%@",
- _baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ _baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret {
- NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPURL, _uniqueId, _deviceName, clientPairSecret];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPSURL, _uniqueId, _deviceName, clientPairSecret];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
- (NSURLRequest*) newPairChallenge {
- if (![self ensureHttpsUrlPopulated:NO]) {
- return nil;
- }
-
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
- (NSURLRequest *)newAppListRequest {
- if (![self ensureHttpsUrlPopulated:NO]) {
- return nil;
- }
-
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
-- (NSURLRequest *)newServerInfoRequest:(bool)fastFail {
- if (_serverCert == nil) {
- // Use HTTP if the cert is not pinned yet
- return [self newHttpServerInfoRequest:fastFail];
- }
-
- if (![self ensureHttpsUrlPopulated:fastFail]) {
- return nil;
- }
-
+- (NSURLRequest *)newServerInfoRequest {
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId];
- return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
-}
-
-- (NSURLRequest *)newHttpServerInfoRequest:(bool)fastFail {
- NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
- return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
- (NSURLRequest *)newHttpServerInfoRequest {
- return [self newHttpServerInfoRequest:false];
+ NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
+ return [self createRequestFromString:urlString enableTimeout:TRUE];
}
-- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config {
- if (![self ensureHttpsUrlPopulated:NO]) {
- return nil;
- }
-
- // Using an FPS value over 60 causes SOPS to default to 720p60,
- // so force it to 0 to ensure the correct resolution is set. We
- // used to use 60 here but that locked the frame rate to 60 FPS
- // on GFE 3.20.3. We do not do this hack for Sunshine (which is
- // indicated by a negative version in the last field.
- int fps = (config.frameRate > 60 && ![config.appVersion containsString:@".-"]) ? 0 : config.frameRate;
-
- NSString* urlString = [NSString stringWithFormat:@"%@/%@?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d&gcpersist=%d%s",
- _baseHTTPSURL, verb, _uniqueId,
- config.appID,
- config.width, config.height, fps,
- config.optimizeGameSettings ? 1 : 0,
- [Utils bytesToHex:config.riKey], config.riKeyId,
- (config.supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
- config.playAudioOnPC ? 1 : 0,
- SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration),
- config.gamepadMask, config.gamepadMask,
- !config.multiController ? 1 : 0,
- LiGetLaunchUrlQueryParameters()];
- Log(LOG_I, @"Requesting: %@", urlString);
+- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid {
+ NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=1&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, appId, width, height, refreshRate, rikey, rikeyid];
// This blocks while the app is launching
- return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
+ return [self createRequestFromString:urlString enableTimeout:FALSE];
+}
+
+- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId {
+ NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, riKey, riKeyId];
+ // This blocks while the app is resuming
+ return [self createRequestFromString:urlString enableTimeout:FALSE];
}
- (NSURLRequest*) newQuitAppRequest {
- if (![self ensureHttpsUrlPopulated:NO]) {
- return nil;
- }
-
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
- return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
+ return [self createRequestFromString:urlString enableTimeout:FALSE];
}
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
- if (![self ensureHttpsUrlPopulated:NO]) {
- return nil;
- }
-
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId];
- return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
+ return [self createRequestFromString:urlString enableTimeout:FALSE];
}
- (NSString*) bytesToHex:(NSData*)data {
@@ -319,7 +196,7 @@
SecIdentityCopyCertificate(identity, &certificate);
- return [[NSArray alloc] initWithObjects:(__bridge_transfer id)certificate, nil];
+ return [[NSArray alloc] initWithObjects:(__bridge id)certificate, nil];
}
// Returns the identity
@@ -331,14 +208,13 @@
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
- CFArrayRef items = nil;
+ CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import(p12Data, options, &items);
if (securityError == errSecSuccess) {
//Log(LOG_D, @"Success opening p12 certificate. Items: %ld", CFArrayGetCount(items));
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
- identityApp = (SecIdentityRef)CFRetain(CFDictionaryGetValue(identityDict, kSecImportItemIdentity));
- CFRelease(items);
+ identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
} else {
Log(LOG_E, @"Error opening Certificate.");
}
@@ -353,36 +229,6 @@
// Allow untrusted server certificates
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
- if (SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust) != 1) {
- Log(LOG_E, @"Server certificate count mismatch");
- completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
- return;
- }
-
- SecCertificateRef actualCert = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, 0);
- if (actualCert == nil) {
- Log(LOG_E, @"Server certificate parsing error");
- completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
- return;
- }
-
- CFDataRef actualCertData = SecCertificateCopyData(actualCert);
- if (actualCertData == nil) {
- Log(LOG_E, @"Server certificate data parsing error");
- completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
- return;
- }
-
- if (!CFEqual(actualCertData, (__bridge CFDataRef)_serverCert)) {
- Log(LOG_E, @"Server certificate mismatch");
- CFRelease(actualCertData);
- completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
- return;
- }
-
- CFRelease(actualCertData);
-
- // Allow TLS handshake to proceed
completionHandler(NSURLSessionAuthChallengeUseCredential,
[NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]);
}
@@ -392,7 +238,6 @@
SecIdentityRef identity = [self getClientCertificate];
NSArray* certArray = [self getCertificate:identity];
NSURLCredential* newCredential = [NSURLCredential credentialWithIdentity:identity certificates:certArray persistence:NSURLCredentialPersistencePermanent];
- CFRelease(identity);
completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
}
else
diff --git a/Limelight/Network/HttpRequest.h b/Limelight/Network/HttpRequest.h
index ace3da2..743b58b 100644
--- a/Limelight/Network/HttpRequest.h
+++ b/Limelight/Network/HttpRequest.h
@@ -6,6 +6,7 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "HttpResponse.h"
@interface HttpRequest : NSObject
diff --git a/Limelight/Network/HttpResponse.h b/Limelight/Network/HttpResponse.h
index 70478a5..28d2636 100644
--- a/Limelight/Network/HttpResponse.h
+++ b/Limelight/Network/HttpResponse.h
@@ -6,6 +6,8 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
+
static NSString* TAG_STATUS_CODE = @"status_code";
static NSString* TAG_STATUS_MESSAGE = @"status_message";
diff --git a/Limelight/Network/HttpResponse.m b/Limelight/Network/HttpResponse.m
index 5cb398d..312825b 100644
--- a/Limelight/Network/HttpResponse.m
+++ b/Limelight/Network/HttpResponse.m
@@ -55,26 +55,21 @@
xmlChar* statusStr = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_CODE UTF8String]);
if (statusStr != NULL) {
- int status = (int)[[NSString stringWithUTF8String:(const char*)statusStr] longLongValue];
+ int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
xmlFree(statusStr);
self.statusCode = status;
}
xmlChar* statusMsgXml = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_MESSAGE UTF8String]);
+ NSString* statusMsg;
if (statusMsgXml != NULL) {
- self.statusMessage = [NSString stringWithUTF8String:(const char*)statusMsgXml];
+ statusMsg = [NSString stringWithUTF8String:(const char*)statusMsgXml];
xmlFree(statusMsgXml);
}
else {
- self.statusMessage = @"Server Error";
- }
-
- if (self.statusCode == -1 && [self.statusMessage isEqualToString:@"Invalid"]) {
- // Special case handling an audio capture error which GFE doesn't
- // provide any useful status message for.
- self.statusCode = 418;
- self.statusMessage = @"Missing audio capture device. Reinstalling GeForce Experience should resolve this error.";
+ statusMsg = @"Server Error";
}
+ self.statusMessage = statusMsg;
node = node->children;
@@ -94,8 +89,6 @@
}
xmlFreeDoc(docPtr);
-
- Log(LOG_D, @"Parsed XML data: %@", _elements);
}
@end
diff --git a/Limelight/Network/MDNSManager.h b/Limelight/Network/MDNSManager.h
index 1c50994..c0932c4 100644
--- a/Limelight/Network/MDNSManager.h
+++ b/Limelight/Network/MDNSManager.h
@@ -6,11 +6,11 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
-#import "TemporaryHost.h"
+#import
@protocol MDNSCallback
-- (void) updateHost:(TemporaryHost*)host;
+- (void) updateHosts:(NSArray*)hosts;
@end
@@ -21,7 +21,6 @@
- (id) initWithCallback:(id) callback;
- (void) searchForHosts;
- (void) stopSearching;
-- (void) forgetHosts;
@end
diff --git a/Limelight/Network/MDNSManager.m b/Limelight/Network/MDNSManager.m
index c0e0265..c35a197 100644
--- a/Limelight/Network/MDNSManager.m
+++ b/Limelight/Network/MDNSManager.m
@@ -9,15 +9,11 @@
#import "MDNSManager.h"
#import "TemporaryHost.h"
-#include
-
-#include
-
@implementation MDNSManager {
NSNetServiceBrowser* mDNSBrowser;
+ NSMutableArray* domains;
NSMutableArray* services;
BOOL scanActive;
- BOOL timerPending;
}
static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
@@ -27,210 +23,43 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
self.callback = callback;
- scanActive = FALSE;
-
mDNSBrowser = [[NSNetServiceBrowser alloc] init];
[mDNSBrowser setDelegate:self];
+ domains = [[NSMutableArray alloc] init];
services = [[NSMutableArray alloc] init];
return self;
}
- (void) searchForHosts {
- if (scanActive) {
- return;
- }
-
Log(LOG_I, @"Starting mDNS discovery");
scanActive = TRUE;
-
- if (!timerPending) {
- timerPending = TRUE;
-
- // Just invoke the timer callback to save a little code
- [self startSearchTimerCallback:nil];
- }
+ [mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
}
- (void) stopSearching {
- if (!scanActive) {
- return;
- }
-
Log(LOG_I, @"Stopping mDNS discovery");
scanActive = FALSE;
[mDNSBrowser stop];
}
-- (void) forgetHosts {
- [services removeAllObjects];
-}
-
-+ (NSString*)sockAddrToString:(NSData*)addrData {
- char addrStr[INET6_ADDRSTRLEN];
- struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
- if (addr->sa_family == AF_INET) {
- inet_ntop(addr->sa_family, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr));
- unsigned short port = ntohs(((struct sockaddr_in*)addr)->sin_port);
- return [NSString stringWithFormat: @"%s:%u", addrStr, port];
- }
- else {
- struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
- inet_ntop(addr->sa_family, &sin6->sin6_addr, addrStr, sizeof(addrStr));
- unsigned short port = ntohs(((struct sockaddr_in6*)addr)->sin6_port);
- if (sin6->sin6_scope_id != 0) {
- // Link-local addresses with scope IDs are special
- return [NSString stringWithFormat: @"[%s%%%u]:%u", addrStr, sin6->sin6_scope_id, port];
- }
- else {
- return [NSString stringWithFormat: @"[%s]:%u", addrStr, port];
+- (NSArray*) getFoundHosts {
+ NSMutableArray* hosts = [[NSMutableArray alloc] init];
+ for (NSNetService* service in services) {
+ if (service.hostName != nil) {
+ TemporaryHost* host = [[TemporaryHost alloc] init];
+ host.activeAddress = host.address = service.hostName;
+ host.name = host.address;
+ [hosts addObject:host];
}
}
-}
-
-+ (BOOL)isAddress:(uint8_t*)address inSubnet:(uint8_t*)subnet netmask:(int)bits {
- for (int i = 0; i < bits; i++) {
- uint8_t mask = 1 << (i % 8);
- if ((address[i / 8] & mask) != (subnet[i / 8] & mask)) {
- return NO;
- }
- }
- return YES;
-}
-
-+ (BOOL)isLocalIpv6Address:(NSData*)addrData {
- struct sockaddr_in6* sin6 = (struct sockaddr_in6*)[addrData bytes];
- if (sin6->sin6_family != AF_INET6) {
- return NO;
- }
-
- uint8_t* addrBytes = sin6->sin6_addr.s6_addr;
- uint8_t prefix[2];
-
- // fe80::/10
- prefix[0] = 0xfe;
- prefix[1] = 0x80;
- if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:10]) {
- // Link-local
- return YES;
- }
-
- // fec0::/10
- prefix[0] = 0xfe;
- prefix[1] = 0xc0;
- if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:10]) {
- // Site local
- return YES;
- }
-
- // fc00::/7
- prefix[0] = 0xfc;
- prefix[1] = 0x00;
- if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:7]) {
- // ULA
- return YES;
- }
-
- return NO;
-}
-
-+ (NSString*)getBestIpv6Address:(NSArray*)addresses {
- for (NSData* addrData in addresses) {
- struct sockaddr_in6* sin6 = (struct sockaddr_in6*)[addrData bytes];
- if (sin6->sin6_family != AF_INET6) {
- continue;
- }
-
- if ([MDNSManager isLocalIpv6Address:addrData]) {
- // Skip non-global addresses
- continue;
- }
-
- uint8_t* addrBytes = sin6->sin6_addr.s6_addr;
- uint8_t prefix[2];
-
- // 2002::/16
- prefix[0] = 0x20;
- prefix[1] = 0x02;
- if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:16]) {
- Log(LOG_I, @"Ignoring 6to4 address: %@", [MDNSManager sockAddrToString:addrData]);
- continue;
- }
-
- // 2001::/32
- prefix[0] = 0x20;
- prefix[1] = 0x01;
- if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:32]) {
- Log(LOG_I, @"Ignoring Teredo address: %@", [MDNSManager sockAddrToString:addrData]);
- continue;
- }
-
- return [MDNSManager sockAddrToString:addrData];
- }
-
- return nil;
+ return hosts;
}
- (void)netServiceDidResolveAddress:(NSNetService *)service {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- NSArray* addresses = [service addresses];
-
- for (NSData* addrData in addresses) {
- Log(LOG_I, @"Resolved address: %@ -> %@", [service hostName], [MDNSManager sockAddrToString: addrData]);
- }
-
- TemporaryHost* host = [[TemporaryHost alloc] init];
-
- // First, look for an IPv4 record for the local address
- for (NSData* addrData in addresses) {
- struct sockaddr_in* sin = (struct sockaddr_in*)[addrData bytes];
- if (sin->sin_family != AF_INET) {
- continue;
- }
-
- // Don't send a STUN request if we're connected to a VPN. We'll likely get the VPN
- // gateway's external address rather than the external address of the LAN.
- if (![Utils isActiveNetworkVPN]) {
- // Since we discovered this host over IPv4 mDNS, we know we're on the same network
- // as the PC and we can use our current WAN address as a likely candidate
- // for our PC's external address.
- struct in_addr wanAddr;
- int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
- if (err == 0) {
- char addrStr[INET_ADDRSTRLEN];
- inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
- host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
- Log(LOG_I, @"External IPv4 address (STUN): %@ -> %@", [service hostName], host.externalAddress);
- }
- else {
- Log(LOG_E, @"STUN failed to get WAN address: %d", err);
- }
- }
-
- host.localAddress = [MDNSManager sockAddrToString:addrData];
- Log(LOG_I, @"Local address chosen: %@ -> %@", [service hostName], host.localAddress);
- break;
- }
-
- if (host.localAddress == nil) {
- // If we didn't find an IPv4 record, look for a local IPv6 record
- for (NSData* addrData in addresses) {
- if ([MDNSManager isLocalIpv6Address:addrData]) {
- host.localAddress = [MDNSManager sockAddrToString:addrData];
- Log(LOG_I, @"Local address chosen: %@ -> %@", [service hostName], host.localAddress);
- break;
- }
- }
- }
-
- host.ipv6Address = [MDNSManager getBestIpv6Address:addresses];
- Log(LOG_I, @"IPv6 address chosen: %@ -> %@", [service hostName], host.ipv6Address);
-
- host.activeAddress = host.localAddress;
- host.name = service.hostName;
- [self.callback updateHost:host];
- });
+ Log(LOG_I, @"Resolved address: %@ -> %@", service, service.hostName);
+ [self.callback updateHosts:[self getFoundHosts]];
}
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
@@ -240,19 +69,17 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
[NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(retryResolveTimerCallback:)
- userInfo:sender
+ userInfo:nil
repeats:NO];
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
- Log(LOG_D, @"Found service: %@", aNetService);
+ Log(LOG_I, @"Found service: %@", aNetService);
+ [aNetService setDelegate:self];
+ [aNetService resolveWithTimeout:5];
- if (![services containsObject:aNetService]) {
- Log(LOG_I, @"Found new host: %@", aNetService.name);
- [aNetService setDelegate:self];
- [aNetService resolveWithTimeout:5];
- [services addObject:aNetService];
- }
+ [services removeObject:aNetService];
+ [services addObject:aNetService];
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
@@ -263,29 +90,23 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict {
Log(LOG_W, @"Did not perform search: \n%@", [errorDict description]);
- // We'll schedule a retry in startSearchTimerCallback
+ // Schedule a retry in 2 seconds
+ [NSTimer scheduledTimerWithTimeInterval:2.0
+ target:self
+ selector:@selector(retrySearchTimerCallback:)
+ userInfo:nil
+ repeats:NO];
}
-- (void)startSearchTimerCallback:(NSTimer *)timer {
+- (void)retrySearchTimerCallback:(NSTimer *)timer {
// Check if we've been stopped since this was queued
if (!scanActive) {
- timerPending = FALSE;
return;
}
- Log(LOG_D, @"Restarting mDNS search");
+ Log(LOG_I, @"Retrying mDNS search");
[mDNSBrowser stop];
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
-
- // Search again in 5 seconds. We need to do this because
- // we want more aggressive querying than Bonjour will normally
- // do for when we're at the hosts screen. This also covers scenarios
- // where discovery didn't work, like if WiFi was disabled.
- [NSTimer scheduledTimerWithTimeInterval:5.0
- target:self
- selector:@selector(startSearchTimerCallback:)
- userInfo:nil
- repeats:NO];
}
- (void)retryResolveTimerCallback:(NSTimer *)timer {
@@ -294,13 +115,13 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
return;
}
- NSNetService* service = timer.userInfo;
- Log(LOG_I, @"Retrying mDNS resolution for %@", service);
-
- if (service.hostName == nil) {
- [service setDelegate:self];
- [service resolveWithTimeout:5];
+ Log(LOG_I, @"Retrying mDNS resolution");
+ for (NSNetService* service in services) {
+ if (service.hostName == nil) {
+ [service setDelegate:self];
+ [service resolveWithTimeout:5];
+ }
}
}
-@end
+@end
\ No newline at end of file
diff --git a/Limelight/Network/PairManager.h b/Limelight/Network/PairManager.h
index 2cc29d2..144eb7d 100644
--- a/Limelight/Network/PairManager.h
+++ b/Limelight/Network/PairManager.h
@@ -6,17 +6,22 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "HttpManager.h"
@protocol PairCallback
-- (void) startPairing:(NSString*)PIN;
-- (void) pairSuccessful:(NSData*)serverCert;
+- (void) showPIN:(NSString*)PIN;
+- (void) pairSuccessful;
- (void) pairFailed:(NSString*)message;
- (void) alreadyPaired;
@end
@interface PairManager : NSOperation
-- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id)callback;
+- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id)callback;
+- (NSString*) generatePIN;
+- (NSData*) saltPIN:(NSString*)PIN;
+- (void) initiatePair:(int)serverMajorVersion;
+
@end
diff --git a/Limelight/Network/PairManager.m b/Limelight/Network/PairManager.m
index bac91b8..7898be4 100644
--- a/Limelight/Network/PairManager.m
+++ b/Limelight/Network/PairManager.m
@@ -17,117 +17,78 @@
@implementation PairManager {
HttpManager* _httpManager;
- NSData* _clientCert;
+ NSData* _cert;
id _callback;
}
-- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id)callback {
+- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id)callback {
self = [super init];
_httpManager = httpManager;
- _clientCert = clientCert;
+ _cert = cert;
_callback = callback;
return self;
}
- (void) main {
- // We have to call startPairing before calling any other _callback functions
- NSString* PIN = [self generatePIN];
- [_callback startPairing:PIN];
-
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
- [_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
+ [_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest]
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
+ if (serverInfoResp == nil) {
+ [_callback pairFailed:@"Unable to connect to PC"];
+ return;
+ }
if ([serverInfoResp isStatusOk]) {
- if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
+ if (![[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_AVAILABLE"]) {
+ [_callback pairFailed:@"You must stop streaming before attempting to pair."];
+ } else if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
if (appversion == nil) {
[_callback pairFailed:@"Missing XML element"];
return;
}
- [self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue] withState:[serverInfoResp getStringTag:@"state"]];
+ [self initiatePair: [[appversion substringToIndex:1] intValue]];
} else {
[_callback alreadyPaired];
}
}
- else {
- [_callback pairFailed:serverInfoResp.statusMessage];
- }
}
-- (void) finishPairing:(UIBackgroundTaskIdentifier)bgId
- forResponse:(HttpResponse*)resp
- withFallbackError:(NSString*)errorMsg {
- [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+- (void) initiatePair:(int)serverMajorVersion {
+ Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
- if (bgId != UIBackgroundTaskInvalid) {
- [[UIApplication sharedApplication] endBackgroundTask:bgId];
- }
-
- if (![resp isStatusOk]) {
- // Use the response error if the request failed
- errorMsg = resp.statusMessage;
- }
-
- [_callback pairFailed:errorMsg];
-}
-
-- (void) finishPairing:(UIBackgroundTaskIdentifier)bgId withSuccess:(NSData*)derCertBytes {
- if (bgId != UIBackgroundTaskInvalid) {
- [[UIApplication sharedApplication] endBackgroundTask:bgId];
- }
-
- [_callback pairSuccessful:derCertBytes];
-}
-
-// All codepaths must call finishPairing exactly once before returning!
-- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion withState:(NSString*)state {
- Log(LOG_I, @"Pairing with generation %d server in state %@", serverMajorVersion, state);
-
- // Start a background task to help prevent the app from being killed
- // while pairing is in progress.
- UIBackgroundTaskIdentifier bgId = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"Pairing PC" expirationHandler:^{
- Log(LOG_W, @"Background pairing time has expired!");
- }];
-
- NSData* salt = [Utils randomBytes:16];
- NSData* saltedPIN = [self concatData:salt with:[PIN dataUsingEncoding:NSUTF8StringEncoding]];
-
- Log(LOG_I, @"PIN: %@, salt %@", PIN, salt);
+ NSString* PIN = [self generatePIN];
+ NSData* salt = [self saltPIN:PIN];
+ Log(LOG_I, @"PIN: %@, saltedPIN: %@", PIN, salt);
+ [_callback showPIN:PIN];
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]]];
if (![self verifyResponseStatus:pairResp]) {
- // GFE does not allow pairing while a server is busy, but Sunshine does. We give it a try and display the busy error if it fails.
- if ([state hasSuffix:@"_SERVER_BUSY"]) {
- [self finishPairing:bgId forResponse:pairResp withFallbackError:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
- }
- else {
- [self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
- }
+ return;
+ }
+ NSInteger pairedStatus;
+ if (![pairResp getIntTag:@"paired" value:&pairedStatus] || !pairedStatus) {
+ [_callback pairFailed:@"Pairing was declined by the target."];
return;
}
NSString* plainCert = [pairResp getStringTag:@"plaincert"];
if ([plainCert length] == 0) {
- [self finishPairing:bgId forResponse:pairResp withFallbackError:@"Another pairing attempt is already in progress."];
+ [_callback pairFailed:@"Another pairing attempt is already in progress."];
return;
}
- // Pin the cert for TLS usage on this host
- NSData* derCertBytes = [CryptoManager pemToDer:[Utils hexToBytes:plainCert]];
- [_httpManager setServerCert:derCertBytes];
-
CryptoManager* cryptoMan = [[CryptoManager alloc] init];
NSData* aesKey;
// Gen 7 servers use SHA256 to get the key
int hashLength;
if (serverMajorVersion >= 7) {
- aesKey = [cryptoMan createAESKeyFromSaltSHA256:saltedPIN];
+ aesKey = [cryptoMan createAESKeyFromSaltSHA256:salt];
hashLength = 32;
}
else {
- aesKey = [cryptoMan createAESKeyFromSaltSHA1:saltedPIN];
+ aesKey = [cryptoMan createAESKeyFromSaltSHA1:salt];
hashLength = 20;
}
@@ -137,7 +98,11 @@
HttpResponse* challengeResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:challengeResp withUrlRequest:[_httpManager newChallengeRequest:encryptedChallenge]]];
if (![self verifyResponseStatus:challengeResp]) {
- [self finishPairing:bgId forResponse:challengeResp withFallbackError:@"Pairing stage #2 failed"];
+ return;
+ }
+ if (![challengeResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Pairing stage #2 failed"];
return;
}
@@ -148,7 +113,7 @@
NSData* serverChallenge = [decServerChallengeResp subdataWithRange:NSMakeRange(hashLength, 16)];
NSData* clientSecret = [Utils randomBytes:16];
- NSData* challengeRespHashInput = [self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_clientCert]] with:clientSecret];
+ NSData* challengeRespHashInput = [self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_cert]] with:clientSecret];
NSData* challengeRespHash;
if (serverMajorVersion >= 7) {
challengeRespHash = [cryptoMan SHA256HashData: challengeRespHashInput];
@@ -156,26 +121,26 @@
else {
challengeRespHash = [cryptoMan SHA1HashData: challengeRespHashInput];
}
-
- assert([challengeRespHash length] <= 32);
- NSMutableData* paddedHash = [NSMutableData dataWithData:challengeRespHash];
- [paddedHash setLength:32];
-
- NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:paddedHash withKey:aesKey];
+ NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:challengeRespHash withKey:aesKey];
HttpResponse* secretResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:secretResp withUrlRequest:[_httpManager newChallengeRespRequest:challengeRespEncrypted]]];
if (![self verifyResponseStatus:secretResp]) {
- [self finishPairing:bgId forResponse:secretResp withFallbackError:@"Pairing stage #3 failed"];
+ return;
+ }
+ if (![secretResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Pairing stage #3 failed"];
return;
}
NSData* serverSecretResp = [Utils hexToBytes:[secretResp getStringTag:@"pairingsecret"]];
NSData* serverSecret = [serverSecretResp subdataWithRange:NSMakeRange(0, 16)];
- NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, serverSecretResp.length - 16)];
+ NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, 256)];
if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
- [self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"];
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Server certificate invalid"];
return;
}
@@ -188,7 +153,8 @@
serverChallengeRespHash = [cryptoMan SHA1HashData: serverChallengeRespHashInput];
}
if (![serverChallengeRespHash isEqual:serverResponse]) {
- [self finishPairing:bgId forResponse:secretResp withFallbackError:@"Incorrect PIN"];
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Incorrect PIN"];
return;
}
@@ -196,32 +162,38 @@
HttpResponse* clientSecretResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientSecretResp withUrlRequest:[_httpManager newClientSecretRespRequest:[Utils bytesToHex:clientPairingSecret]]]];
if (![self verifyResponseStatus:clientSecretResp]) {
- [self finishPairing:bgId forResponse:clientSecretResp withFallbackError:@"Pairing stage #4 failed"];
+ return;
+ }
+ if (![clientSecretResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Pairing stage #4 failed"];
return;
}
HttpResponse* clientPairChallengeResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientPairChallengeResp withUrlRequest:[_httpManager newPairChallenge]]];
if (![self verifyResponseStatus:clientPairChallengeResp]) {
- [self finishPairing:bgId forResponse:clientPairChallengeResp withFallbackError:@"Pairing stage #5 failed"];
return;
}
-
- [self finishPairing:bgId withSuccess:derCertBytes];
+ if (![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Pairing stage #5 failed"];
+ return;
+ }
+ [_callback pairSuccessful];
}
-// Caller calls finishPairing for us on failure
- (BOOL) verifyResponseStatus:(HttpResponse*)resp {
- if (![resp isStatusOk]) {
+ if (resp == nil) {
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:@"Network error occured."];
+ return false;
+ } else if (![resp isStatusOk]) {
+ [_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
+ [_callback pairFailed:resp.statusMessage];
return false;
} else {
- NSInteger pairedStatus;
-
- if (![resp getIntTag:@"paired" value:&pairedStatus]) {
- return false;
- }
-
- return pairedStatus == 1;
+ return true;
}
}
@@ -233,9 +205,16 @@
- (NSString*) generatePIN {
NSString* PIN = [NSString stringWithFormat:@"%d%d%d%d",
- arc4random_uniform(10), arc4random_uniform(10),
- arc4random_uniform(10), arc4random_uniform(10)];
+ arc4random() % 10, arc4random() % 10,
+ arc4random() % 10, arc4random() % 10];
return PIN;
}
+- (NSData*) saltPIN:(NSString*)PIN {
+ NSMutableData* saltedPIN = [[NSMutableData alloc] initWithCapacity:20];
+ [saltedPIN appendData:[Utils randomBytes:16]];
+ [saltedPIN appendBytes:[PIN UTF8String] length:4];
+ return saltedPIN;
+}
+
@end
diff --git a/Limelight/Network/ServerInfoResponse.h b/Limelight/Network/ServerInfoResponse.h
index dab491c..d9c0fc2 100644
--- a/Limelight/Network/ServerInfoResponse.h
+++ b/Limelight/Network/ServerInfoResponse.h
@@ -11,7 +11,6 @@
#define TAG_HOSTNAME @"hostname"
#define TAG_EXTERNAL_IP @"ExternalIP"
-#define TAG_HTTPS_PORT @"HttpsPort"
#define TAG_LOCAL_IP @"LocalIP"
#define TAG_UNIQUE_ID @"uniqueid"
#define TAG_MAC_ADDRESS @"mac"
@@ -19,9 +18,6 @@
#define TAG_STATE @"state"
#define TAG_CURRENT_GAME @"currentgame"
-// Sunshine extension
-#define TAG_EXTERNAL_PORT @"ExternalPort"
-
@interface ServerInfoResponse : HttpResponse
- (void) populateWithData:(NSData *)data;
diff --git a/Limelight/Network/ServerInfoResponse.m b/Limelight/Network/ServerInfoResponse.m
index e210369..3faf632 100644
--- a/Limelight/Network/ServerInfoResponse.m
+++ b/Limelight/Network/ServerInfoResponse.m
@@ -19,86 +19,26 @@
- (void) populateHost:(TemporaryHost*)host {
host.name = [[self getStringTag:TAG_HOSTNAME] trim];
+ host.externalAddress = [[self getStringTag:TAG_EXTERNAL_IP] trim];
+ host.localAddress = [[self getStringTag:TAG_LOCAL_IP] trim];
host.uuid = [[self getStringTag:TAG_UNIQUE_ID] trim];
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] trim];
- NSInteger httpsPort;
- if ([self getIntTag:TAG_HTTPS_PORT value:&httpsPort]) {
- host.httpsPort = (unsigned short)httpsPort;
- }
- else {
- // Use the default if it's not specified
- host.httpsPort = 47984;
- }
-
- // We might get an IPv4 loopback address if we're using GS IPv6 Forwarder
- NSString *lanAddr = [[self getStringTag:TAG_LOCAL_IP] trim];
- if (![lanAddr hasPrefix:@"127."]) {
- unsigned short localPort;
-
- // If we reached this host through this port, store our port there
- if (host.activeAddress && [lanAddr isEqualToString:[Utils addressPortStringToAddress:host.activeAddress]]) {
- localPort = [Utils addressPortStringToPort:host.activeAddress];
- }
- else if (host.localAddress) {
- // If there's an existing local address, use the port from that
- localPort = [Utils addressPortStringToPort:host.localAddress];
- }
- else {
- // If all else fails, use 47989
- localPort = 47989;
- }
-
- host.localAddress = [Utils addressAndPortToAddressPortString:lanAddr port:localPort];
- }
-
- // This is a Sunshine extension for WAN port remapping
- NSInteger externalHttpPort;
- if (![self getIntTag:TAG_EXTERNAL_PORT value:&externalHttpPort]) {
- // Use our active port if it's not specified
- if (host.activeAddress) {
- externalHttpPort = [Utils addressPortStringToPort:host.activeAddress];
- }
- else {
- // Otherwise use the default
- externalHttpPort = 47989;
- }
- }
-
- // Modern GFE versions don't actually give us a WAN address anymore
- // so we leave the one that we populated from mDNS discovery via STUN.
- NSString *wanAddr = [[self getStringTag:TAG_EXTERNAL_IP] trim];
- if (wanAddr) {
- host.externalAddress = [Utils addressAndPortToAddressPortString:wanAddr port:externalHttpPort];
- }
- else if (host.externalAddress) {
- // If we have an external address (via STUN) already, we still need to populate the port
- host.externalAddress = [Utils addressAndPortToAddressPortString:[Utils addressPortStringToAddress:host.externalAddress] port:externalHttpPort];
- }
-
NSString *state = [[self getStringTag:TAG_STATE] trim];
- if (![state hasSuffix:@"_SERVER_BUSY"]) {
+ if ([state hasSuffix:@"_SERVER_AVAILABLE"]) {
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
// has the semantics that its name would indicate. To contain the effects of this change as much
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
host.currentGame = @"0";
}
- // GFE uses the Mjolnir codename in their state enum values
- host.isNvidiaServerSoftware = [state containsString:@"MJOLNIR"];
-
NSInteger pairStatus;
if ([self getIntTag:TAG_PAIR_STATUS value:&pairStatus]) {
host.pairState = pairStatus ? PairStatePaired : PairStateUnpaired;
} else {
host.pairState = PairStateUnknown;
}
-
- NSString *serverCodecModeString = [self getStringTag:@"ServerCodecModeSupport"];
- if (serverCodecModeString != nil) {
- host.serverCodecModeSupport = [[serverCodecModeString trim] intValue];
- }
}
@end
diff --git a/Limelight/Network/WakeOnLanManager.h b/Limelight/Network/WakeOnLanManager.h
index f1d0828..a3da81e 100644
--- a/Limelight/Network/WakeOnLanManager.h
+++ b/Limelight/Network/WakeOnLanManager.h
@@ -6,6 +6,7 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
+#import
#import "TemporaryHost.h"
@interface WakeOnLanManager : NSObject
diff --git a/Limelight/Network/WakeOnLanManager.m b/Limelight/Network/WakeOnLanManager.m
index cd679e0..35fd3fb 100644
--- a/Limelight/Network/WakeOnLanManager.m
+++ b/Limelight/Network/WakeOnLanManager.m
@@ -16,15 +16,8 @@
@implementation WakeOnLanManager
-static const int numStaticPorts = 2;
-static const int staticPorts[numStaticPorts] = {
- 9, // Standard WOL port (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
-};
+static const int numPorts = 5;
+static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
+ (void) populateAddress:(struct sockaddr_storage*)addr withPort:(unsigned short)port {
if (addr->ss_family == AF_INET) {
@@ -40,34 +33,26 @@ static const int dynamicPorts[numDynamicPorts] = {
+ (void) wakeHost:(TemporaryHost*)host {
NSData* wolPayload = [WakeOnLanManager createPayload:host];
- for (int i = 0; i < 5; i++) {
- NSString* address;
+ for (int i = 0; i < 3; i++) {
+ const char* address;
struct addrinfo hints, *res, *curr;
// try all ip addresses
if (i == 0 && host.localAddress != nil) {
- address = host.localAddress;
+ address = [host.localAddress UTF8String];
} else if (i == 1 && host.externalAddress != nil) {
- address = host.externalAddress;
+ address = [host.externalAddress UTF8String];
} else if (i == 2 && host.address != nil) {
- address = host.address;
- } else if (i == 3 && host.ipv6Address != nil) {
- address = host.ipv6Address;
- } else if (i == 4) {
- address = @"255.255.255.255";
+ address = [host.address UTF8String];
} else {
// Requested address wasn't present
continue;
}
- // Get the raw address and base port from the address+port string
- NSString* rawAddress = [Utils addressPortStringToAddress:address];
- unsigned short basePort = [Utils addressPortStringToPort:address];
-
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_flags = AI_ADDRCONFIG;
- if (getaddrinfo([rawAddress UTF8String], NULL, &hints, &res) != 0 || res == NULL) {
+ if (getaddrinfo(address, NULL, &hints, &res) != 0 || res == NULL) {
// Failed to resolve address
Log(LOG_E, @"Failed to resolve WOL address");
continue;
@@ -78,7 +63,6 @@ static const int dynamicPorts[numDynamicPorts] = {
// may be different address families.
for (curr = res; curr != NULL; curr = curr->ai_next) {
int wolSocket;
- int val;
wolSocket = socket(curr->ai_family, SOCK_DGRAM, IPPROTO_UDP);
if (wolSocket < 0) {
@@ -86,36 +70,19 @@ static const int dynamicPorts[numDynamicPorts] = {
continue;
}
- val = 1;
- setsockopt(wolSocket, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val));
-
struct sockaddr_storage addr;
memset(&addr, 0, sizeof(addr));
memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
- for (int j = 0; j < numStaticPorts; j++) {
- [WakeOnLanManager populateAddress:&addr withPort:staticPorts[j]];
+ for (int j = 0; j < numPorts; j++) {
+ [WakeOnLanManager populateAddress:&addr withPort:ports[j]];
long err = sendto(wolSocket,
[wolPayload bytes],
[wolPayload length],
0,
(struct sockaddr*)&addr,
curr->ai_addrlen);
- Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", staticPorts[j], err);
- }
-
- for (int j = 0; j < numDynamicPorts; j++) {
- // Offset the WoL dynamic ports by the base port
- unsigned short port = ((int)dynamicPorts[j] - 47989) + basePort;
-
- [WakeOnLanManager populateAddress:&addr withPort:port];
- long err = sendto(wolSocket,
- [wolPayload bytes],
- [wolPayload length],
- 0,
- (struct sockaddr*)&addr,
- curr->ai_addrlen);
- Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", port, err);
+ Log(LOG_I, @"Sending WOL packet returned: %ld", err);
}
close(wolSocket);
diff --git a/Limelight/Stream/Connection.h b/Limelight/Stream/Connection.h
index dc15541..62b1774 100644
--- a/Limelight/Stream/Connection.h
+++ b/Limelight/Stream/Connection.h
@@ -6,29 +6,27 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "VideoDecoderRenderer.h"
#import "StreamConfiguration.h"
-#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
+@protocol ConnectionCallbacks
-typedef struct {
- CFTimeInterval startTime;
- CFTimeInterval endTime;
- int totalFrames;
- int receivedFrames;
- int networkDroppedFrames;
- int totalHostProcessingLatency;
- int framesWithHostProcessingLatency;
- int maxHostProcessingLatency;
- int minHostProcessingLatency;
-} video_stats_t;
+- (void) connectionStarted;
+- (void) connectionTerminated:(long)errorCode;
+- (void) stageStarting:(const char*)stageName;
+- (void) stageComplete:(const char*)stageName;
+- (void) stageFailed:(const char*)stageName withError:(long)errorCode;
+- (void) launchFailed:(NSString*)message;
+- (void) displayMessage:(const char*)message;
+- (void) displayTransientMessage:(const char*)message;
+
+@end
@interface Connection : NSOperation
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id)callbacks;
-(void) terminate;
-(void) main;
--(BOOL) getVideoStats:(video_stats_t*)stats;
--(NSString*) getActiveCodecName;
@end
diff --git a/Limelight/Stream/Connection.m b/Limelight/Stream/Connection.m
index 33361fc..4460486 100644
--- a/Limelight/Stream/Connection.m
+++ b/Limelight/Stream/Connection.m
@@ -7,15 +7,12 @@
//
#import "Connection.h"
-#import "Utils.h"
-#import
-
-#define SDL_MAIN_HANDLED
-#import
+#import
+#import
#include "Limelight.h"
-#include "opus_multistream.h"
+#include "opus.h"
@implementation Connection {
SERVER_INFORMATION _serverInfo;
@@ -23,286 +20,208 @@
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
DECODER_RENDERER_CALLBACKS _drCallbacks;
AUDIO_RENDERER_CALLBACKS _arCallbacks;
- char _hostString[256];
- char _appVersionString[32];
- char _gfeVersionString[32];
- char _rtspSessionUrl[128];
}
static NSLock* initLock;
-static OpusMSDecoder* opusDecoder;
+static OpusDecoder *opusDecoder;
static id _callbacks;
-static int lastFrameNumber;
-static int activeVideoFormat;
-static video_stats_t currentVideoStats;
-static video_stats_t lastVideoStats;
-static NSLock* videoStatsLock;
-static SDL_AudioDeviceID audioDevice;
-static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
-static void* audioBuffer;
-static int audioFrameSize;
+#define PCM_BUFFER_SIZE 1024
+#define OUTPUT_BUS 0
+struct AUDIO_BUFFER_QUEUE_ENTRY {
+ struct AUDIO_BUFFER_QUEUE_ENTRY *next;
+ int length;
+ int offset;
+ char data[0];
+};
+
+#define MAX_QUEUE_ENTRIES 10
+
+static short decodedPcmBuffer[512];
+static NSLock *audioLock;
+static struct AUDIO_BUFFER_QUEUE_ENTRY *audioBufferQueue;
+static int audioBufferQueueLength;
+static AudioComponentInstance audioUnit;
static VideoDecoderRenderer* renderer;
-int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
-{
- [renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
- lastFrameNumber = 0;
- activeVideoFormat = videoFormat;
- memset(¤tVideoStats, 0, sizeof(currentVideoStats));
- memset(&lastVideoStats, 0, sizeof(lastVideoStats));
- return 0;
-}
-
-void DrStart(void)
-{
- [renderer start];
-}
-
-void DrStop(void)
-{
- [renderer stop];
-}
-
--(BOOL) getVideoStats:(video_stats_t*)stats
-{
- // We return lastVideoStats because it is a complete 1 second window
- [videoStatsLock lock];
- if (lastVideoStats.endTime != 0) {
- memcpy(stats, &lastVideoStats, sizeof(*stats));
- [videoStatsLock unlock];
- return YES;
- }
-
- // No stats yet
- [videoStatsLock unlock];
- return NO;
-}
-
--(NSString*) getActiveCodecName
-{
- switch (activeVideoFormat)
- {
- case VIDEO_FORMAT_H264:
- return @"H.264";
- case VIDEO_FORMAT_H264_HIGH8_444:
- return @"H.264 (4:4:4)";
- case VIDEO_FORMAT_H265:
- return @"HEVC";
- case VIDEO_FORMAT_H265_REXT8_444:
- return @"HEVC (4:4:4)";
- case VIDEO_FORMAT_H265_MAIN10:
- if (LiGetCurrentHostDisplayHdrMode()) {
- return @"HEVC Main 10 HDR";
- }
- else {
- return @"HEVC Main 10 SDR";
- }
- case VIDEO_FORMAT_H265_REXT10_444:
- if (LiGetCurrentHostDisplayHdrMode()) {
- return @"HEVC Main 10 HDR (4:4:4)";
- }
- else {
- return @"HEVC Main 10 SDR (4:4:4)";
- }
- case VIDEO_FORMAT_AV1_MAIN8:
- return @"AV1";
- case VIDEO_FORMAT_AV1_HIGH8_444:
- return @"AV1 (4:4:4)";
- case VIDEO_FORMAT_AV1_MAIN10:
- if (LiGetCurrentHostDisplayHdrMode()) {
- return @"AV1 10-bit HDR";
- }
- else {
- return @"AV1 10-bit SDR";
- }
- case VIDEO_FORMAT_AV1_HIGH10_444:
- if (LiGetCurrentHostDisplayHdrMode()) {
- return @"AV1 10-bit HDR (4:4:4)";
- }
- else {
- return @"AV1 10-bit SDR (4:4:4)";
- }
- default:
- return @"UNKNOWN";
- }
-}
-
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
{
int offset = 0;
- int ret;
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
if (data == NULL) {
// A frame was lost due to OOM condition
return DR_NEED_IDR;
}
- CFTimeInterval now = CACurrentMediaTime();
- if (!lastFrameNumber) {
- currentVideoStats.startTime = now;
- lastFrameNumber = decodeUnit->frameNumber;
- }
- else {
- // Flip stats roughly every second
- if (now - currentVideoStats.startTime >= 1.0f) {
- currentVideoStats.endTime = now;
-
- [videoStatsLock lock];
- lastVideoStats = currentVideoStats;
- [videoStatsLock unlock];
-
- memset(¤tVideoStats, 0, sizeof(currentVideoStats));
- currentVideoStats.startTime = now;
- }
-
- // Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
- currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
- currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
- lastFrameNumber = decodeUnit->frameNumber;
- }
-
- if (decodeUnit->frameHostProcessingLatency != 0) {
- if (currentVideoStats.minHostProcessingLatency == 0 || decodeUnit->frameHostProcessingLatency < currentVideoStats.minHostProcessingLatency) {
- currentVideoStats.minHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
- }
-
- if (decodeUnit->frameHostProcessingLatency > currentVideoStats.maxHostProcessingLatency) {
- currentVideoStats.maxHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
- }
-
- currentVideoStats.framesWithHostProcessingLatency++;
- currentVideoStats.totalHostProcessingLatency += decodeUnit->frameHostProcessingLatency;
- }
-
- currentVideoStats.receivedFrames++;
- currentVideoStats.totalFrames++;
-
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
- // Submit parameter set NALUs directly since no copy is required by the decoder
- if (entry->bufferType != BUFFER_TYPE_PICDATA) {
- ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
- length:entry->length
- bufferType:entry->bufferType
- decodeUnit:decodeUnit];
- if (ret != DR_OK) {
- free(data);
- return ret;
- }
- }
- else {
- memcpy(&data[offset], entry->data, entry->length);
- offset += entry->length;
- }
-
+ memcpy(&data[offset], entry->data, entry->length);
+ offset += entry->length;
entry = entry->next;
}
-
- // This function will take our picture data buffer
- return [renderer submitDecodeBuffer:data
- length:offset
- bufferType:BUFFER_TYPE_PICDATA
- decodeUnit:decodeUnit];
+
+ // This function will take our buffer
+ return [renderer submitDecodeBuffer:data length:decodeUnit->fullLength];
}
-int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
+void ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig)
{
int err;
- SDL_AudioSpec want, have;
- if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
- Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError());
- return -1;
- }
-
- SDL_zero(want);
- want.freq = opusConfig->sampleRate;
- want.format = AUDIO_S16;
- want.channels = opusConfig->channelCount;
- want.samples = opusConfig->samplesPerFrame;
-
- audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
- if (audioDevice == 0) {
- Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
- ArCleanup();
- return -1;
+ // We only support stereo for now
+ assert(audioConfiguration == AUDIO_CONFIGURATION_STEREO);
+
+ opusDecoder = opus_decoder_create(opusConfig->sampleRate,
+ opusConfig->channelCount,
+ &err);
+
+ audioLock = [[NSLock alloc] init];
+
+ // Configure the audio session for our app
+ NSError *audioSessionError = nil;
+ AVAudioSession* audioSession = [AVAudioSession sharedInstance];
+
+ [audioSession setPreferredSampleRate:opusConfig->sampleRate error:&audioSessionError];
+ [audioSession setCategory: AVAudioSessionCategoryPlayback error: &audioSessionError];
+ [audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
+ [audioSession setPreferredIOBufferDuration:0.005 error:&audioSessionError];
+ [audioSession setActive: YES error: &audioSessionError];
+
+ OSStatus status;
+
+ AudioComponentDescription audioDesc;
+ audioDesc.componentType = kAudioUnitType_Output;
+ audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
+ audioDesc.componentFlags = 0;
+ audioDesc.componentFlagsMask = 0;
+ audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
+
+ status = AudioComponentInstanceNew(AudioComponentFindNext(NULL, &audioDesc), &audioUnit);
+
+ if (status) {
+ Log(LOG_E, @"Unable to instantiate new AudioComponent: %d", (int32_t)status);
}
- audioConfig = *opusConfig;
- audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
- audioBuffer = SDL_malloc(audioFrameSize);
- if (audioBuffer == NULL) {
- Log(LOG_E, @"Failed to allocate audio frame buffer");
- ArCleanup();
- return -1;
+ 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 = AudioUnitSetProperty(audioUnit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input,
+ OUTPUT_BUS,
+ &audioFormat,
+ sizeof(audioFormat));
+ if (status) {
+ Log(LOG_E, @"Unable to set audio unit to input: %d", (int32_t)status);
}
- opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
- opusConfig->channelCount,
- opusConfig->streams,
- opusConfig->coupledStreams,
- opusConfig->mapping,
- &err);
- if (opusDecoder == NULL) {
- Log(LOG_E, @"Failed to create Opus decoder");
- ArCleanup();
- return -1;
+ AURenderCallbackStruct callbackStruct = {0};
+ callbackStruct.inputProc = playbackCallback;
+ callbackStruct.inputProcRefCon = NULL;
+
+ status = AudioUnitSetProperty(audioUnit,
+ kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input,
+ OUTPUT_BUS,
+ &callbackStruct,
+ sizeof(callbackStruct));
+ if (status) {
+ Log(LOG_E, @"Unable to set audio unit callback: %d", (int32_t)status);
}
- // Start playback
- SDL_PauseAudioDevice(audioDevice, 0);
+ status = AudioUnitInitialize(audioUnit);
+ if (status) {
+ Log(LOG_E, @"Unable to initialize audioUnit: %d", (int32_t)status);
+ }
- // Disable lowering volume of other audio streams (SDL sets AVAudioSessionCategoryOptionDuckOthers by default)
- [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
-
- return 0;
+ status = AudioOutputUnitStart(audioUnit);
+ if (status) {
+ Log(LOG_E, @"Unable to start audioUnit: %d", (int32_t)status);
+ }
}
void ArCleanup(void)
{
if (opusDecoder != NULL) {
- opus_multistream_decoder_destroy(opusDecoder);
+ opus_decoder_destroy(opusDecoder);
opusDecoder = NULL;
}
- if (audioDevice != 0) {
- SDL_CloseAudioDevice(audioDevice);
- audioDevice = 0;
+ OSStatus status = AudioOutputUnitStop(audioUnit);
+ if (status) {
+ Log(LOG_E, @"Unable to stop audioUnit: %d", (int32_t)status);
}
- if (audioBuffer != NULL) {
- SDL_free(audioBuffer);
- audioBuffer = NULL;
+ status = AudioUnitUninitialize(audioUnit);
+ if (status) {
+ Log(LOG_E, @"Unable to uninitialize audioUnit: %d", (int32_t)status);
}
- SDL_QuitSubSystem(SDL_INIT_AUDIO);
+ // Audio session is now inactive
+ AVAudioSession* audioSession = [AVAudioSession sharedInstance];
+ [audioSession setActive: YES error: nil];
+
+ // This is safe because we're guaranteed that nobody
+ // is touching this list now
+ struct AUDIO_BUFFER_QUEUE_ENTRY *entry;
+ while (audioBufferQueue != NULL) {
+ entry = audioBufferQueue;
+ audioBufferQueue = entry->next;
+ audioBufferQueueLength--;
+ free(entry);
+ }
}
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
{
- int decodeLen;
-
- // Don't queue if there's already more than 30 ms of audio data waiting
- // in Moonlight's audio queue.
- if (LiGetPendingAudioDuration() > 30) {
- return;
- }
-
- decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
- (short*)audioBuffer, audioConfig.samplesPerFrame, 0);
- if (decodeLen > 0) {
- // Provide backpressure on the queue to ensure too many frames don't build up
- // in SDL's audio queue.
- while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
- SDL_Delay(1);
- }
+ int decodedLength = opus_decode(opusDecoder, (unsigned char*)sampleData, sampleLength, decodedPcmBuffer, PCM_BUFFER_SIZE / 2, 0);
+ if (decodedLength > 0) {
+ // Return of opus_decode is samples per channel
+ decodedLength *= 4;
- if (SDL_QueueAudio(audioDevice,
- audioBuffer,
- sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
- Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
+ struct AUDIO_BUFFER_QUEUE_ENTRY *newEntry = malloc(sizeof(*newEntry) + decodedLength);
+ if (newEntry != NULL) {
+ newEntry->next = NULL;
+ newEntry->length = decodedLength;
+ newEntry->offset = 0;
+ memcpy(newEntry->data, decodedPcmBuffer, decodedLength);
+
+ [audioLock lock];
+ if (audioBufferQueueLength > MAX_QUEUE_ENTRIES) {
+ Log(LOG_W, @"Audio player too slow. Dropping all decoded samples!");
+
+ // Clear all values from the buffer queue
+ struct AUDIO_BUFFER_QUEUE_ENTRY *entry;
+ while (audioBufferQueue != NULL) {
+ entry = audioBufferQueue;
+ audioBufferQueue = entry->next;
+ audioBufferQueueLength--;
+ free(entry);
+ }
+ }
+
+ if (audioBufferQueue == NULL) {
+ audioBufferQueue = newEntry;
+ }
+ else {
+ struct AUDIO_BUFFER_QUEUE_ENTRY *lastEntry = audioBufferQueue;
+ while (lastEntry->next != NULL) {
+ lastEntry = lastEntry->next;
+ }
+ lastEntry->next = newEntry;
+ }
+ audioBufferQueueLength++;
+
+ [audioLock unlock];
}
}
}
@@ -317,9 +236,9 @@ void ClStageComplete(int stage)
[_callbacks stageComplete:LiGetStageName(stage)];
}
-void ClStageFailed(int stage, int errorCode)
+void ClStageFailed(int stage, long errorCode)
{
- [_callbacks stageFailed:LiGetStageName(stage) withError:errorCode portTestFlags:LiGetPortFlagsFromStage(stage)];
+ [_callbacks stageFailed:LiGetStageName(stage) withError:errorCode];
}
void ClConnectionStarted(void)
@@ -327,58 +246,23 @@ void ClConnectionStarted(void)
[_callbacks connectionStarted];
}
-void ClConnectionTerminated(int errorCode)
+void ClConnectionTerminated(long errorCode)
{
[_callbacks connectionTerminated: errorCode];
}
-void ClLogMessage(const char* format, ...)
+void ClDisplayMessage(const char* message)
{
- va_list va;
- va_start(va, format);
- vfprintf(stderr, format, va);
- va_end(va);
+ [_callbacks displayMessage: message];
}
-void ClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
+void ClDisplayTransientMessage(const char* message)
{
- [_callbacks rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
-}
-
-void ClConnectionStatusUpdate(int status)
-{
- [_callbacks connectionStatusUpdate:status];
-}
-
-void ClSetHdrMode(bool enabled)
-{
- [renderer setHdrMode:enabled];
- [_callbacks setHdrMode:enabled];
-}
-
-void ClRumbleTriggers(uint16_t controllerNumber, uint16_t leftTriggerMotor, uint16_t rightTriggerMotor)
-{
- [_callbacks rumbleTriggers:controllerNumber leftTrigger:leftTriggerMotor rightTrigger:rightTriggerMotor];
-}
-
-void ClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
-{
- [_callbacks setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
-}
-
-void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
-{
- [_callbacks setControllerLed:controllerNumber r:r g:g b:b];
+ [_callbacks displayTransientMessage: message];
}
-(void) terminate
{
- // Interrupt any action blocking LiStartConnection(). This is
- // thread-safe and done outside initLock on purpose, since we
- // won't be able to acquire it if LiStartConnection is in
- // progress.
- LiInterruptConnection();
-
// We dispatch this async to get out because this can be invoked
// on a thread inside common and we don't want to deadlock. It also avoids
// blocking on the caller's thread waiting to acquire initLock.
@@ -392,109 +276,138 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id)callbacks
{
self = [super init];
-
+
// Use a lock to ensure that only one thread is initializing
// or deinitializing a connection at a time.
if (initLock == nil) {
initLock = [[NSLock alloc] init];
}
- if (videoStatsLock == nil) {
- videoStatsLock = [[NSLock alloc] init];
+ LiInitializeServerInformation(&_serverInfo);
+ _serverInfo.address = [config.host cStringUsingEncoding:NSUTF8StringEncoding];
+ _serverInfo.serverInfoAppVersion = [config.appVersion cStringUsingEncoding:NSUTF8StringEncoding];
+ if (config.gfeVersion != nil) {
+ _serverInfo.serverInfoGfeVersion = [config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding];
}
- NSString *rawAddress = [Utils addressPortStringToAddress:config.host];
- strncpy(_hostString,
- [rawAddress cStringUsingEncoding:NSUTF8StringEncoding],
- sizeof(_hostString) - 1);
- strncpy(_appVersionString,
- [config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
- sizeof(_appVersionString) - 1);
- if (config.gfeVersion != nil) {
- strncpy(_gfeVersionString,
- [config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
- sizeof(_gfeVersionString) - 1);
- }
- if (config.rtspSessionUrl != nil) {
- strncpy(_rtspSessionUrl,
- [config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
- sizeof(_rtspSessionUrl) - 1);
- }
-
- LiInitializeServerInformation(&_serverInfo);
- _serverInfo.address = _hostString;
- _serverInfo.serverInfoAppVersion = _appVersionString;
- if (config.gfeVersion != nil) {
- _serverInfo.serverInfoGfeVersion = _gfeVersionString;
- }
- if (config.rtspSessionUrl != nil) {
- _serverInfo.rtspSessionUrl = _rtspSessionUrl;
- }
- _serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
-
renderer = myRenderer;
_callbacks = callbacks;
-
+
LiInitializeStreamConfiguration(&_streamConfig);
_streamConfig.width = config.width;
_streamConfig.height = config.height;
_streamConfig.fps = config.frameRate;
_streamConfig.bitrate = config.bitRate;
- _streamConfig.supportedVideoFormats = config.supportedVideoFormats;
- _streamConfig.audioConfiguration = config.audioConfiguration;
- // Since we require iOS 12 or above, we're guaranteed to be running
- // on a 64-bit device with ARMv8 crypto instructions, so we don't
- // need to check for that here.
- _streamConfig.encryptionFlags = ENCFLG_ALL;
+ // FIXME: We should use 1024 when streaming remotely
+ _streamConfig.packetSize = 1292;
- if ([Utils isActiveNetworkVPN]) {
- // Force remote streaming mode when a VPN is connected
- _streamConfig.streamingRemotely = STREAM_CFG_REMOTE;
- _streamConfig.packetSize = 1024;
- }
- else {
- // Detect remote streaming automatically based on the IP address of the target
- _streamConfig.streamingRemotely = STREAM_CFG_AUTO;
- _streamConfig.packetSize = 1392;
- }
-
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
memset(_streamConfig.remoteInputAesIv, 0, 16);
int riKeyId = htonl(config.riKeyId);
memcpy(_streamConfig.remoteInputAesIv, &riKeyId, sizeof(riKeyId));
-
+
LiInitializeVideoCallbacks(&_drCallbacks);
- _drCallbacks.setup = DrDecoderSetup;
- _drCallbacks.start = DrStart;
- _drCallbacks.stop = DrStop;
- _drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
- CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
- CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
-
+ _drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
+
LiInitializeAudioCallbacks(&_arCallbacks);
_arCallbacks.init = ArInit;
_arCallbacks.cleanup = ArCleanup;
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
- _arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
-
+
LiInitializeConnectionCallbacks(&_clCallbacks);
_clCallbacks.stageStarting = ClStageStarting;
_clCallbacks.stageComplete = ClStageComplete;
_clCallbacks.stageFailed = ClStageFailed;
_clCallbacks.connectionStarted = ClConnectionStarted;
_clCallbacks.connectionTerminated = ClConnectionTerminated;
- _clCallbacks.logMessage = ClLogMessage;
- _clCallbacks.rumble = ClRumble;
- _clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
- _clCallbacks.setHdrMode = ClSetHdrMode;
- _clCallbacks.rumbleTriggers = ClRumbleTriggers;
- _clCallbacks.setMotionEventState = ClSetMotionEventState;
- _clCallbacks.setControllerLED = ClSetControllerLED;
-
+ _clCallbacks.displayMessage = ClDisplayMessage;
+ _clCallbacks.displayTransientMessage = ClDisplayTransientMessage;
+
return self;
}
+static OSStatus playbackCallback(void *inRefCon,
+ AudioUnitRenderActionFlags *ioActionFlags,
+ const AudioTimeStamp *inTimeStamp,
+ UInt32 inBusNumber,
+ UInt32 inNumberFrames,
+ AudioBufferList *ioData) {
+ // Notes: ioData contains buffers (may be more than one!)
+ // Fill them up as much as you can. Remember to set the size value in each buffer to match how
+ // much data is in the buffer.
+
+ bool ranOutOfData = false;
+ for (int i = 0; i < ioData->mNumberBuffers; i++) {
+ ioData->mBuffers[i].mNumberChannels = 2;
+
+ if (ranOutOfData) {
+ ioData->mBuffers[i].mDataByteSize = 0;
+ continue;
+ }
+
+ if (ioData->mBuffers[i].mDataByteSize != 0) {
+ int thisBufferOffset = 0;
+
+ FillBufferAgain:
+ // Make sure there's data to write
+ if (ioData->mBuffers[i].mDataByteSize - thisBufferOffset == 0) {
+ continue;
+ }
+
+ struct AUDIO_BUFFER_QUEUE_ENTRY *audioEntry = NULL;
+
+ [audioLock lock];
+ if (audioBufferQueue != NULL) {
+ // Dequeue this entry temporarily
+ audioEntry = audioBufferQueue;
+ audioBufferQueue = audioBufferQueue->next;
+ audioBufferQueueLength--;
+ }
+ [audioLock unlock];
+
+ if (audioEntry == NULL) {
+ // No data left
+ ranOutOfData = true;
+ ioData->mBuffers[i].mDataByteSize = thisBufferOffset;
+ continue;
+ }
+
+ // Figure out how much data we can write
+ int min = MIN(ioData->mBuffers[i].mDataByteSize - thisBufferOffset, audioEntry->length);
+
+ // Copy data to the audio buffer
+ memcpy(&ioData->mBuffers[i].mData[thisBufferOffset], &audioEntry->data[audioEntry->offset], min);
+ thisBufferOffset += min;
+
+ if (min < audioEntry->length) {
+ // This entry still has unused data
+ audioEntry->length -= min;
+ audioEntry->offset += min;
+
+ // Requeue the entry
+ [audioLock lock];
+ audioEntry->next = audioBufferQueue;
+ audioBufferQueue = audioEntry;
+ audioBufferQueueLength++;
+ [audioLock unlock];
+ }
+ else {
+ // This entry is fully depleted so free it
+ free(audioEntry);
+
+ // Try to grab another sample to fill this buffer with
+ goto FillBufferAgain;
+ }
+
+ ioData->mBuffers[i].mDataByteSize = thisBufferOffset;
+ }
+ }
+
+ return noErr;
+}
+
+
-(void) main
{
[initLock lock];
@@ -503,7 +416,6 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
&_clCallbacks,
&_drCallbacks,
&_arCallbacks,
- NULL, 0,
NULL, 0);
[initLock unlock];
}
diff --git a/Limelight/Stream/ConnectionCallbacks.h b/Limelight/Stream/ConnectionCallbacks.h
deleted file mode 100644
index f9c13b4..0000000
--- a/Limelight/Stream/ConnectionCallbacks.h
+++ /dev/null
@@ -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
-
-- (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
diff --git a/Limelight/Stream/StreamConfiguration.h b/Limelight/Stream/StreamConfiguration.h
index e912b57..7ce1ba4 100644
--- a/Limelight/Stream/StreamConfiguration.h
+++ b/Limelight/Stream/StreamConfiguration.h
@@ -6,30 +6,19 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
+
@interface StreamConfiguration : NSObject
@property NSString* host;
-@property unsigned short httpsPort;
@property NSString* appVersion;
@property NSString* gfeVersion;
@property NSString* appID;
-@property NSString* appName;
-@property NSString* rtspSessionUrl;
-@property int serverCodecModeSupport;
@property int width;
@property int height;
@property int frameRate;
@property int bitRate;
@property int riKeyId;
@property NSData* riKey;
-@property int gamepadMask;
-@property BOOL optimizeGameSettings;
-@property BOOL playAudioOnPC;
-@property BOOL swapABXYButtons;
-@property int audioConfiguration;
-@property int supportedVideoFormats;
-@property BOOL multiController;
-@property BOOL useFramePacing;
-@property NSData* serverCert;
@end
diff --git a/Limelight/Stream/StreamConfiguration.m b/Limelight/Stream/StreamConfiguration.m
index 4d308cd..e463739 100644
--- a/Limelight/Stream/StreamConfiguration.m
+++ b/Limelight/Stream/StreamConfiguration.m
@@ -9,5 +9,5 @@
#import "StreamConfiguration.h"
@implementation StreamConfiguration
-@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport;
+@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey;
@end
diff --git a/Limelight/Stream/StreamManager.h b/Limelight/Stream/StreamManager.h
index 277d6fe..b2285e1 100644
--- a/Limelight/Stream/StreamManager.h
+++ b/Limelight/Stream/StreamManager.h
@@ -6,15 +6,13 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
#import "StreamConfiguration.h"
#import "Connection.h"
@interface StreamManager : NSOperation
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id)callback;
-
- (void) stopStream;
-- (NSString*) getStatsOverlayText;
-
@end
diff --git a/Limelight/Stream/StreamManager.m b/Limelight/Stream/StreamManager.m
index e776fee..f3d7d15 100644
--- a/Limelight/Stream/StreamManager.m
+++ b/Limelight/Stream/StreamManager.m
@@ -10,18 +10,15 @@
#import "CryptoManager.h"
#import "HttpManager.h"
#import "Utils.h"
-
+#import "OnScreenControls.h"
#import "StreamView.h"
#import "ServerInfoResponse.h"
#import "HttpResponse.h"
#import "HttpRequest.h"
#import "IdManager.h"
-#include
-
@implementation StreamManager {
StreamConfiguration* _config;
-
UIView* _renderView;
id _callbacks;
Connection* _connection;
@@ -37,24 +34,25 @@
return self;
}
+
- (void)main {
- [CryptoManager generateKeyPairUsingSSL];
+ [CryptoManager generateKeyPairUsingSSl];
+ NSString* uniqueId = [IdManager getUniqueId];
+ NSData* cert = [CryptoManager readCertFromFile];
- HttpManager* hMan = [[HttpManager alloc] initWithAddress:_config.host httpsPort:_config.httpsPort
- serverCert:_config.serverCert];
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
+ uniqueId:uniqueId
+ deviceName:@"roth"
+ cert:cert];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
NSString* pairStatus = [serverInfoResp getStringTag:@"PairStatus"];
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
NSString* gfeVersion = [serverInfoResp getStringTag:@"GfeVersion"];
NSString* serverState = [serverInfoResp getStringTag:@"state"];
- if (![serverInfoResp isStatusOk]) {
- [_callbacks launchFailed:serverInfoResp.statusMessage];
- return;
- }
- else if (pairStatus == NULL || appversion == NULL || serverState == NULL) {
+ if (![serverInfoResp isStatusOk] || pairStatus == NULL || appversion == NULL || serverState == NULL) {
[_callbacks launchFailed:@"Failed to connect to PC"];
return;
}
@@ -65,45 +63,34 @@
return;
}
- // Only perform this check on GFE (as indicated by MJOLNIR in state value)
- if ((_config.width > 4096 || _config.height > 4096) && [serverState containsString:@"MJOLNIR"]) {
- // Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K.
- // We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation.
- NSString* codecSupport = [serverInfoResp getStringTag:@"ServerCodecModeSupport"];
- if (codecSupport == nil || !([codecSupport intValue] & 0x200)) {
- [_callbacks launchFailed:@"Your host PC's GPU doesn't support streaming video resolutions over 4K."];
+ // resumeApp and launchApp handle calling launchFailed
+ if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
+ // App already running, resume it
+ if (![self resumeApp:hMan]) {
+ return;
+ }
+ } else {
+ // Start app
+ if (![self launchApp:hMan]) {
return;
}
}
+ // Set mouse delta factors from the screen resolution and stream size
+ CGFloat screenScale = [[UIScreen mainScreen] scale];
+ CGRect screenBounds = [[UIScreen mainScreen] bounds];
+ CGSize screenSize = CGSizeMake(screenBounds.size.width * screenScale, screenBounds.size.height * screenScale);
+ [((StreamView*)_renderView) setMouseDeltaFactors:_config.width / screenSize.width
+ y:_config.height / screenSize.height];
+
// Populate the config's version fields from serverinfo
_config.appVersion = appversion;
_config.gfeVersion = gfeVersion;
- // resumeApp and launchApp handle calling launchFailed
- NSString* sessionUrl;
- if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
- // App already running, resume it
- if (![self resumeApp:hMan receiveSessionUrl:&sessionUrl]) {
- return;
- }
- } else {
- // Start app
- if (![self launchApp:hMan receiveSessionUrl:&sessionUrl]) {
- return;
- }
- }
-
- // Populate RTSP session URL from launch/resume response
- _config.rtspSessionUrl = sessionUrl;
-
- // Initializing the renderer must be done on the main thread
- dispatch_async(dispatch_get_main_queue(), ^{
- VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks streamAspectRatio:(float)self->_config.width / (float)self->_config.height useFramePacing:self->_config.useFramePacing];
- self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
- NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
- [opQueue addOperation:self->_connection];
- });
+ VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc]initWithView:_renderView];
+ _connection = [[Connection alloc] initWithConfig:_config renderer:renderer connectionCallbacks:_callbacks];
+ NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
+ [opQueue addOperation:_connection];
}
- (void) stopStream
@@ -111,82 +98,46 @@
[_connection terminate];
}
-- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
+- (BOOL) launchApp:(HttpManager*)hMan {
HttpResponse* launchResp = [[HttpResponse alloc] init];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"launch" config:_config]]];
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:
+ [hMan newLaunchRequest:_config.appID
+ width:_config.width
+ height:_config.height
+ refreshRate:_config.frameRate
+ rikey:[Utils bytesToHex:_config.riKey]
+ rikeyid:_config.riKeyId]]];
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
- if (![launchResp isStatusOk]) {
- [_callbacks launchFailed:launchResp.statusMessage];
+ if (launchResp == NULL || ![launchResp isStatusOk]) {
+ [_callbacks launchFailed:@"Failed to launch app"];
Log(LOG_E, @"Failed Launch Response: %@", launchResp.statusMessage);
return FALSE;
} else if (gameSession == NULL || [gameSession isEqualToString:@"0"]) {
- [_callbacks launchFailed:@"Failed to launch app"];
- Log(LOG_E, @"Failed to parse game session");
+ [_callbacks launchFailed:launchResp.statusMessage];
+ Log(LOG_E, @"Failed to parse game session. Code: %ld Response: %@", (long)launchResp.statusCode, launchResp.statusMessage);
return FALSE;
}
- *sessionUrl = [launchResp getStringTag:@"sessionUrl0"];
return TRUE;
}
-- (BOOL) resumeApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
+- (BOOL) resumeApp:(HttpManager*)hMan {
HttpResponse* resumeResp = [[HttpResponse alloc] init];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"resume" config:_config]]];
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:
+ [hMan newResumeRequestWithRiKey:[Utils bytesToHex:_config.riKey]
+ riKeyId:_config.riKeyId]]];
NSString* resume = [resumeResp getStringTag:@"resume"];
- if (![resumeResp isStatusOk]) {
- [_callbacks launchFailed:resumeResp.statusMessage];
+ if (resumeResp == NULL || ![resumeResp isStatusOk]) {
+ [_callbacks launchFailed:@"Failed to resume app"];
Log(LOG_E, @"Failed Resume Response: %@", resumeResp.statusMessage);
return FALSE;
} else if (resume == NULL || [resume isEqualToString:@"0"]) {
- [_callbacks launchFailed:@"Failed to resume app"];
- Log(LOG_E, @"Failed to parse resume response");
+ [_callbacks launchFailed:resumeResp.statusMessage];
+ Log(LOG_E, @"Failed to parse resume response. Code: %ld Response: %@", (long)resumeResp.statusCode, resumeResp.statusMessage);
return FALSE;
}
- *sessionUrl = [resumeResp getStringTag:@"sessionUrl0"];
return TRUE;
}
-- (NSString*) getStatsOverlayText {
- video_stats_t stats;
-
- if (!_connection) {
- return nil;
- }
-
- if (![_connection getVideoStats:&stats]) {
- return nil;
- }
-
- uint32_t rtt, variance;
- NSString* latencyString;
- if (LiGetEstimatedRttInfo(&rtt, &variance)) {
- latencyString = [NSString stringWithFormat:@"%u ms (variance: %u ms)", rtt, variance];
- }
- else {
- latencyString = @"N/A";
- }
-
- NSString* hostProcessingString;
- if (stats.framesWithHostProcessingLatency != 0) {
- hostProcessingString = [NSString stringWithFormat:@"\nHost processing latency min/max/avg: %.1f/%.1f/%.1f ms",
- stats.minHostProcessingLatency / 10.f,
- stats.maxHostProcessingLatency / 10.f,
- (float)stats.totalHostProcessingLatency / stats.framesWithHostProcessingLatency / 10.f];
- }
- else {
- hostProcessingString = @"";
- }
-
- float interval = stats.endTime - stats.startTime;
- return [NSString stringWithFormat:@"Video stream: %dx%d %.2f FPS (Codec: %@)\nFrames dropped by your network connection: %.2f%%\nAverage network latency: %@%@",
- _config.width,
- _config.height,
- stats.totalFrames / interval,
- [_connection getActiveCodecName],
- stats.networkDroppedFrames / interval,
- latencyString,
- hostProcessingString];
-}
-
@end
diff --git a/Limelight/Stream/VideoDecoderRenderer.h b/Limelight/Stream/VideoDecoderRenderer.h
index 0513374..e1ef307 100644
--- a/Limelight/Stream/VideoDecoderRenderer.h
+++ b/Limelight/Stream/VideoDecoderRenderer.h
@@ -6,21 +6,16 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
+
@import AVFoundation;
-#import "ConnectionCallbacks.h"
-
-#include "Limelight.h"
-
@interface VideoDecoderRenderer : NSObject
-- (id)initWithView:(UIView*)view callbacks:(id)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
+- (id)initWithView:(UIView*)view;
-- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
-- (void)start;
-- (void)stop;
-- (void)setHdrMode:(BOOL)enabled;
+- (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 decodeUnit:(PDECODE_UNIT)du;
+- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length;
@end
diff --git a/Limelight/Stream/VideoDecoderRenderer.m b/Limelight/Stream/VideoDecoderRenderer.m
index 5c89e5a..736ace8 100644
--- a/Limelight/Stream/VideoDecoderRenderer.m
+++ b/Limelight/Stream/VideoDecoderRenderer.m
@@ -7,34 +7,17 @@
//
#import "VideoDecoderRenderer.h"
-#import "StreamView.h"
-#include
-#include
-#include
-#include
-#include
-
-// Private libavformat API for writing the AV1 Codec Configuration Box
-extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
- int write_seq_header);
+#include "Limelight.h"
@implementation VideoDecoderRenderer {
- StreamView* _view;
- id _callbacks;
- float _streamAspectRatio;
+ UIView *_view;
AVSampleBufferDisplayLayer* displayLayer;
- int videoFormat;
- int frameRate;
+ Boolean waitingForSps, waitingForPps;
- NSMutableArray *parameterSetBuffers;
- NSData *masteringDisplayColorVolume;
- NSData *contentLightLevelInfo;
+ NSData *spsData, *ppsData;
CMVideoFormatDescriptionRef formatDesc;
-
- CADisplayLink* _displayLink;
- BOOL framePacing;
}
- (void)reinitializeDisplayLayer
@@ -42,26 +25,10 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
CALayer *oldLayer = displayLayer;
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
+ displayLayer.bounds = _view.bounds;
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
-
- // Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio
- // of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that
- // respects the PAR encoded in the SPS which causes our computed video-relative
- // touch location to be wrong in StreamView if the aspect ratio of the host
- // desktop doesn't match the aspect ratio of the stream.
- CGSize videoSize;
- if (_view.bounds.size.width > _view.bounds.size.height * _streamAspectRatio) {
- videoSize = CGSizeMake(_view.bounds.size.height * _streamAspectRatio, _view.bounds.size.height);
- } else {
- videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio);
- }
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
- displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);
- displayLayer.videoGravity = AVLayerVideoGravityResize;
-
- // Hide the layer until we get an IDR frame. This ensures we
- // can see the loading progress label as the stream is starting.
- displayLayer.hidden = YES;
+ displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
if (oldLayer != nil) {
// Switch out the old display layer with the new one
@@ -71,614 +38,237 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
[_view.layer addSublayer:displayLayer];
}
+ // We need some parameter sets before we can properly start decoding frames
+ waitingForSps = true;
+ spsData = nil;
+ waitingForPps = true;
+ ppsData = nil;
+
if (formatDesc != nil) {
CFRelease(formatDesc);
formatDesc = nil;
}
}
-- (id)initWithView:(StreamView*)view callbacks:(id)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing
+- (id)initWithView:(UIView*)view
{
self = [super init];
_view = view;
- _callbacks = callbacks;
- _streamAspectRatio = aspectRatio;
- framePacing = useFramePacing;
-
- parameterSetBuffers = [[NSMutableArray alloc] init];
[self reinitializeDisplayLayer];
return self;
}
-- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate
-{
- self->videoFormat = videoFormat;
- self->frameRate = frameRate;
-}
-
-- (void)start
-{
- _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
- if (@available(iOS 15.0, tvOS 15.0, *)) {
- _displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->frameRate, self->frameRate, self->frameRate);
- }
- else {
- _displayLink.preferredFramesPerSecond = self->frameRate;
- }
- [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
-}
-
-// TODO: Refactor this
-int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
-
-- (void)displayLinkCallback:(CADisplayLink *)sender
-{
- VIDEO_FRAME_HANDLE handle;
- PDECODE_UNIT du;
-
- while (LiPollNextVideoFrame(&handle, &du)) {
- LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
-
- if (framePacing) {
- // Calculate the actual display refresh rate
- double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
-
- // Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
- // Battery saver, accessibility settings, or device thermals can cause the actual
- // refresh rate of the display to drop below the physical maximum.
- if (displayRefreshRate >= frameRate * 0.9f) {
- // Keep one pending frame to smooth out gaps due to
- // network jitter at the cost of 1 frame of latency
- if (LiGetPendingVideoFrames() == 1) {
- break;
- }
- }
- }
- }
-}
-
-- (void)stop
-{
- [_displayLink invalidate];
-}
-
+#define FRAME_START_PREFIX_SIZE 4
#define NALU_START_PREFIX_SIZE 3
#define NAL_LENGTH_PREFIX_SIZE 4
+#define NAL_TYPE_SPS 0x7
+#define NAL_TYPE_PPS 0x8
-- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength
+- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
{
OSStatus status;
- size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer);
+ size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
- // 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;
+ // If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
+ // so it can handle freeing it when the block buffer is destroyed
+ if (offset == 1) {
+ int dataLength = nalLength - NALU_START_PREFIX_SIZE;
+
+ // Pass the real buffer pointer directly (no offset)
+ // This will give it to the block buffer to free when it's released.
+ // All further calls to CMBlockBufferAppendMemoryBlock will do so
+ // at an offset and will not be asking the buffer to be freed.
+ status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
+ nalLength + 1, // Add 1 for the offset we decremented
+ kCFAllocatorDefault,
+ NULL, 0, nalLength + 1, 0);
+ if (status != noErr) {
+ Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
+ return;
+ }
+
+ // Write the length prefix to existing buffer
+ const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
+ (uint8_t)(dataLength >> 8), (uint8_t)dataLength};
+ status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
+ oldOffset, NAL_LENGTH_PREFIX_SIZE);
+ if (status != noErr) {
+ Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
+ return;
+ }
+ } else {
+ // Append a 4 byte buffer to this block for the length prefix
+ status = CMBlockBufferAppendMemoryBlock(existingBuffer, NULL,
+ NAL_LENGTH_PREFIX_SIZE,
+ kCFAllocatorDefault, NULL, 0,
+ NAL_LENGTH_PREFIX_SIZE, 0);
+ if (status != noErr) {
+ Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
+ return;
+ }
+
+ // Write the length prefix to the new buffer
+ int dataLength = nalLength - NALU_START_PREFIX_SIZE;
+ const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
+ (uint8_t)(dataLength >> 8), (uint8_t)dataLength};
+ status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
+ oldOffset, NAL_LENGTH_PREFIX_SIZE);
+ if (status != noErr) {
+ Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
+ return;
+ }
+
+ // Attach the buffer by reference to the block buffer
+ status = CMBlockBufferAppendMemoryBlock(existingBuffer, &data[offset+NALU_START_PREFIX_SIZE],
+ dataLength,
+ kCFAllocatorNull, // Don't deallocate data on free
+ NULL, 0, dataLength, 0);
+ if (status != noErr) {
+ Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
+ return;
+ }
}
}
-- (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 {
- data = nil;
- }
-
- av_free(av1cBuf);
- return data;
-}
-
-// Much of this logic comes from Chrome
-- (CMVideoFormatDescriptionRef)createAV1FormatDescriptionForIDRFrame:(NSData*)frameData {
- NSMutableDictionary* extensions = [[NSMutableDictionary alloc] init];
-
- 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;
- }
-
- AVPacket avPacket = {};
- avPacket.data = (uint8_t*)frameData.bytes;
- avPacket.size = (int)frameData.length;
-
- // Read the sequence header OBU
- CodedBitstreamFragment cbsFrag = {};
- err = ff_cbs_read_packet(cbsCtx, &cbsFrag, &avPacket);
- if (err < 0) {
- Log(LOG_E, @"ff_cbs_read_packet() failed: %d", err);
- ff_cbs_close(&cbsCtx);
- return nil;
- }
-
-#define SET_CFSTR_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (__bridge NSString*)(value)
-#define SET_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (value)
-
- SET_EXTENSION(kCMFormatDescriptionExtension_FormatName, @"av01");
-
- // We use the value for YUV without alpha, same as Chrome
- // https://developer.apple.com/library/archive/qa/qa1183/_index.html
- SET_EXTENSION(kCMFormatDescriptionExtension_Depth, @24);
-
- CodedBitstreamAV1Context* bitstreamCtx = (CodedBitstreamAV1Context*)cbsCtx->priv_data;
- AV1RawSequenceHeader* seqHeader = bitstreamCtx->sequence_header;
- if (seqHeader == NULL) {
- Log(LOG_E, @"AV1 sequence header not found in IDR frame!");
- ff_cbs_fragment_free(&cbsFrag);
- ff_cbs_close(&cbsCtx);
- return nil;
- }
-
- switch (seqHeader->color_config.color_primaries) {
- case 1: // CP_BT_709
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
- kCMFormatDescriptionColorPrimaries_ITU_R_709_2);
- break;
-
- case 6: // CP_BT_601
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
- kCMFormatDescriptionColorPrimaries_SMPTE_C);
- break;
-
- case 9: // CP_BT_2020
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
- kCMFormatDescriptionColorPrimaries_ITU_R_2020);
- break;
-
- default:
- Log(LOG_W, @"Unsupported color_primaries value: %d", seqHeader->color_config.color_primaries);
- break;
- }
-
- switch (seqHeader->color_config.transfer_characteristics) {
- case 1: // TC_BT_709
- case 6: // TC_BT_601
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
- kCMFormatDescriptionTransferFunction_ITU_R_709_2);
- break;
-
- case 7: // TC_SMPTE_240
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
- kCMFormatDescriptionTransferFunction_SMPTE_240M_1995);
- break;
-
- case 8: // TC_LINEAR
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
- kCMFormatDescriptionTransferFunction_Linear);
- break;
-
- case 14: // TC_BT_2020_10_BIT
- case 15: // TC_BT_2020_12_BIT
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
- kCMFormatDescriptionTransferFunction_ITU_R_2020);
- break;
-
- case 16: // TC_SMPTE_2084
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
- kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ);
- break;
-
- case 17: // TC_HLG
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
- kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG);
- break;
-
- default:
- Log(LOG_W, @"Unsupported transfer_characteristics value: %d", seqHeader->color_config.transfer_characteristics);
- break;
- }
-
- switch (seqHeader->color_config.matrix_coefficients) {
- case 1: // MC_BT_709
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
- kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2);
- break;
-
- case 6: // MC_BT_601
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
- kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4);
- break;
-
- case 7: // MC_SMPTE_240
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
- kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995);
- break;
-
- case 9: // MC_BT_2020_NCL
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
- kCMFormatDescriptionYCbCrMatrix_ITU_R_2020);
- break;
-
- default:
- Log(LOG_W, @"Unsupported matrix_coefficients value: %d", seqHeader->color_config.matrix_coefficients);
- break;
- }
-
- SET_EXTENSION(kCMFormatDescriptionExtension_FullRangeVideo, @(seqHeader->color_config.color_range == 1));
-
- // Progressive content
- SET_EXTENSION(kCMFormatDescriptionExtension_FieldCount, @(1));
-
- switch (seqHeader->color_config.chroma_sample_position) {
- case 1: // CSP_VERTICAL
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
- kCMFormatDescriptionChromaLocation_Left);
- break;
-
- case 2: // CSP_COLOCATED
- SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
- kCMFormatDescriptionChromaLocation_TopLeft);
- break;
-
- default:
- Log(LOG_W, @"Unsupported chroma_sample_position value: %d", seqHeader->color_config.chroma_sample_position);
- break;
- }
-
- if (contentLightLevelInfo) {
- SET_EXTENSION(kCMFormatDescriptionExtension_ContentLightLevelInfo, contentLightLevelInfo);
- }
-
- if (masteringDisplayColorVolume) {
- SET_EXTENSION(kCMFormatDescriptionExtension_MasteringDisplayColorVolume, masteringDisplayColorVolume);
- }
-
- // Referenced the VP9 code in Chrome that performs a similar function
- // https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/mac/vt_config_util.mm;drc=977dc02c431b4979e34c7792bc3d646f649dacb4;l=155
- extensions[(__bridge NSString*)kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms] =
- @{
- @"av1C" : [self getAv1CodecConfigurationBox:frameData],
- };
- extensions[@"BitsPerComponent"] = @(bitstreamCtx->bit_depth);
-
-#undef SET_EXTENSION
-#undef SET_CFSTR_EXTENSION
-
- // AV1 doesn't have a special format description function like H.264 and HEVC have, so we just use the generic one
- CMVideoFormatDescriptionRef formatDesc = NULL;
- OSStatus status = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_AV1,
- bitstreamCtx->frame_width, bitstreamCtx->frame_height,
- (__bridge CFDictionaryRef)extensions,
- &formatDesc);
- if (status != noErr) {
- Log(LOG_E, @"Failed to create AV1 format description: %d", (int)status);
- formatDesc = NULL;
- }
-
- ff_cbs_fragment_free(&cbsFrag);
- ff_cbs_close(&cbsCtx);
- return formatDesc;
-}
-
-// This function must free data for bufferType == BUFFER_TYPE_PICDATA
-- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du
+// This function must free data
+- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length
{
+ unsigned char nalType = data[FRAME_START_PREFIX_SIZE] & 0x1F;
OSStatus status;
- // Construct a new format description object each time we receive an IDR frame
- if (du->frameType == FRAME_TYPE_IDR) {
- if (bufferType != BUFFER_TYPE_PICDATA) {
- if (bufferType == BUFFER_TYPE_VPS || bufferType == BUFFER_TYPE_SPS || bufferType == BUFFER_TYPE_PPS) {
- // Add new parameter set into the parameter set array
- int startLen = data[2] == 0x01 ? 3 : 4;
- [parameterSetBuffers addObject:[NSData dataWithBytes:&data[startLen] length:length - startLen]];
- }
-
- // Data is NOT to be freed here. It's a direct usage of the caller's buffer.
-
- // No frame data to submit for these NALUs
- return DR_OK;
- }
-
- // Create the new format description when we get the first picture data buffer of an IDR frame.
- // This is the only way we know that there is no more CSD for this frame.
- //
- // NB: This logic depends on the fact that we submit all picture data in one buffer!
-
- // Free the old format description
- if (formatDesc != NULL) {
- CFRelease(formatDesc);
- formatDesc = NULL;
- }
-
- if (videoFormat & VIDEO_FORMAT_MASK_H264) {
- // Construct parameter set arrays for the format description
- size_t parameterSetCount = [parameterSetBuffers count];
- const uint8_t* parameterSetPointers[parameterSetCount];
- size_t parameterSetSizes[parameterSetCount];
- for (int i = 0; i < parameterSetCount; i++) {
- NSData* parameterSet = parameterSetBuffers[i];
- parameterSetPointers[i] = parameterSet.bytes;
- parameterSetSizes[i] = parameterSet.length;
- }
-
- Log(LOG_I, @"Constructing new H264 format description");
- status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
- parameterSetCount,
- parameterSetPointers,
- parameterSetSizes,
- NAL_LENGTH_PREFIX_SIZE,
- &formatDesc);
- if (status != noErr) {
- Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
- formatDesc = NULL;
- }
-
- // Free parameter set buffers after submission
- [parameterSetBuffers removeAllObjects];
- }
- else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
- // Construct parameter set arrays for the format description
- size_t parameterSetCount = [parameterSetBuffers count];
- const uint8_t* parameterSetPointers[parameterSetCount];
- size_t parameterSetSizes[parameterSetCount];
- for (int i = 0; i < parameterSetCount; i++) {
- NSData* parameterSet = parameterSetBuffers[i];
- parameterSetPointers[i] = parameterSet.bytes;
- parameterSetSizes[i] = parameterSet.length;
- }
-
- Log(LOG_I, @"Constructing new HEVC format description");
-
- NSMutableDictionary* videoFormatParams = [[NSMutableDictionary alloc] init];
-
- if (contentLightLevelInfo) {
- [videoFormatParams setObject:contentLightLevelInfo forKey:(__bridge NSString*)kCMFormatDescriptionExtension_ContentLightLevelInfo];
- }
-
- if (masteringDisplayColorVolume) {
- [videoFormatParams setObject:masteringDisplayColorVolume forKey:(__bridge NSString*)kCMFormatDescriptionExtension_MasteringDisplayColorVolume];
- }
-
- status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
- parameterSetCount,
- parameterSetPointers,
- parameterSetSizes,
- NAL_LENGTH_PREFIX_SIZE,
- (__bridge CFDictionaryRef)videoFormatParams,
- &formatDesc);
-
- if (status != noErr) {
- Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
- formatDesc = NULL;
- }
-
- // Free parameter set buffers after submission
- [parameterSetBuffers removeAllObjects];
- }
- else if (videoFormat & VIDEO_FORMAT_MASK_AV1) {
- NSData* fullFrameData = [NSData dataWithBytesNoCopy:data length:length freeWhenDone:NO];
-
- Log(LOG_I, @"Constructing new AV1 format description");
- formatDesc = [self createAV1FormatDescriptionForIDRFrame:fullFrameData];
- }
- else {
- // Unsupported codec!
- abort();
- }
- }
-
- if (formatDesc == NULL) {
- // Can't decode if we haven't gotten our parameter sets yet
- free(data);
- return DR_NEED_IDR;
- }
-
// Check for previous decoder errors before doing anything
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
- // Recreate the display layer. We are already on the main thread,
- // so this is safe to do right here.
+ // Recreate the display layer
[self reinitializeDisplayLayer];
// Request an IDR frame to initialize the new decoder
+ return DR_NEED_IDR;
+ }
+
+ if (nalType == NAL_TYPE_SPS || nalType == NAL_TYPE_PPS) {
+ if (nalType == NAL_TYPE_SPS) {
+ Log(LOG_I, @"Got SPS");
+ spsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
+ waitingForSps = false;
+
+ // We got a new SPS so wait for a new PPS to match it
+ waitingForPps = true;
+ } else if (nalType == NAL_TYPE_PPS) {
+ Log(LOG_I, @"Got PPS");
+ ppsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
+ waitingForPps = false;
+ }
+
+ // See if we've got all the parameter sets we need
+ if (!waitingForSps && !waitingForPps) {
+ const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
+ const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
+
+ Log(LOG_I, @"Constructing new format description");
+ status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
+ 2, /* count of parameter sets */
+ parameterSetPointers,
+ parameterSetSizes,
+ NAL_LENGTH_PREFIX_SIZE,
+ &formatDesc);
+ if (status != noErr) {
+ Log(LOG_E, @"Failed to create format description: %d", (int)status);
+ formatDesc = NULL;
+ }
+ }
+
+ // Free the data buffer
+ free(data);
+
+ // No frame data to submit for these NALUs
+ return DR_OK;
+ }
+
+ if (formatDesc == NULL) {
+ // Can't decode if we haven't gotten our parameter sets yet
+ free(data);
+ return DR_OK;
+ }
+
+ if (nalType != 0x1 && nalType != 0x5) {
+ // Don't submit parameter set data
+ free(data);
+ return DR_OK;
+ }
+
+ // Now we're decoding actual frame data here
+ CMBlockBufferRef blockBuffer;
+
+ status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
+ if (status != noErr) {
+ Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
free(data);
return DR_NEED_IDR;
}
- // Now we're decoding actual frame data here
- CMBlockBufferRef frameBlockBuffer;
- CMBlockBufferRef dataBlockBuffer;
+ int lastOffset = -1;
+ for (int i = 0; i < length - FRAME_START_PREFIX_SIZE; i++) {
+ // Search for a NALU
+ if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
+ // It's the start of a new NALU
+ if (lastOffset != -1) {
+ // We've seen a start before this so enqueue that NALU
+ [self updateBufferForRange:blockBuffer data:data offset:lastOffset length:i - lastOffset];
+ }
+
+ lastOffset = i;
+ }
+ }
- status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorDefault, NULL, 0, length, 0, &dataBlockBuffer);
- if (status != noErr) {
- Log(LOG_E, @"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status);
- free(data);
- return DR_NEED_IDR;
+ if (lastOffset != -1) {
+ // Enqueue the remaining data
+ [self updateBufferForRange:blockBuffer data:data offset:lastOffset length:length - lastOffset];
}
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
- status = CMBlockBufferCreateEmpty(NULL, 0, 0, &frameBlockBuffer);
- if (status != noErr) {
- Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
- CFRelease(dataBlockBuffer);
- return DR_NEED_IDR;
- }
-
- // H.264 and HEVC formats require NAL prefix fixups from Annex B to length-delimited
- if (videoFormat & (VIDEO_FORMAT_MASK_H264 | VIDEO_FORMAT_MASK_H265)) {
- int lastOffset = -1;
- for (int i = 0; i < length - NALU_START_PREFIX_SIZE; i++) {
- // Search for a NALU
- if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
- // It's the start of a new NALU
- if (lastOffset != -1) {
- // We've seen a start before this so enqueue that NALU
- [self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:i - lastOffset];
- }
-
- lastOffset = i;
- }
- }
-
- if (lastOffset != -1) {
- // Enqueue the remaining data
- [self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:length - lastOffset];
- }
- }
- else {
- // For formats that require no length-changing fixups, just append a reference to the raw data block
- status = CMBlockBufferAppendBufferReference(frameBlockBuffer, dataBlockBuffer, 0, length, 0);
- if (status != noErr) {
- Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
- return DR_NEED_IDR;
- }
- }
-
CMSampleBufferRef sampleBuffer;
- CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeUs, 1000000), kCMTimeInvalid};
-
- status = CMSampleBufferCreateReady(kCFAllocatorDefault,
- frameBlockBuffer,
- formatDesc, 1, 1,
- &sampleTiming, 0, NULL,
+ status = CMSampleBufferCreate(kCFAllocatorDefault,
+ blockBuffer,
+ true, NULL,
+ NULL, formatDesc, 1, 0,
+ NULL, 0, NULL,
&sampleBuffer);
if (status != noErr) {
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
- CFRelease(dataBlockBuffer);
- CFRelease(frameBlockBuffer);
+ CFRelease(blockBuffer);
return DR_NEED_IDR;
}
-
- // Enqueue the next frame
- [self->displayLayer enqueueSampleBuffer:sampleBuffer];
- if (du->frameType == FRAME_TYPE_IDR) {
- // Ensure the layer is visible now
- self->displayLayer.hidden = NO;
-
- // Tell our parent VC to hide the progress indicator
- [self->_callbacks videoContentShown];
+ CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
+ CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
+
+ CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
+ CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
+
+ if (nalType == 1) {
+ // 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);
}
+ [displayLayer enqueueSampleBuffer:sampleBuffer];
+
// Dereference the buffers
- CFRelease(dataBlockBuffer);
- CFRelease(frameBlockBuffer);
+ CFRelease(blockBuffer);
CFRelease(sampleBuffer);
return DR_OK;
}
-- (void)setHdrMode:(BOOL)enabled {
- SS_HDR_METADATA hdrMetadata;
-
- BOOL hasMetadata = enabled && LiGetHdrMetadata(&hdrMetadata);
- BOOL metadataChanged = NO;
-
- if (hasMetadata && hdrMetadata.displayPrimaries[0].x != 0 && hdrMetadata.maxDisplayLuminance != 0) {
- // This data is all in big-endian
- struct {
- vector_ushort2 primaries[3];
- vector_ushort2 white_point;
- uint32_t luminance_max;
- uint32_t luminance_min;
- } __attribute__((packed, aligned(4))) mdcv;
-
- // mdcv is in GBR order while SS_HDR_METADATA is in RGB order
- mdcv.primaries[0].x = __builtin_bswap16(hdrMetadata.displayPrimaries[1].x);
- mdcv.primaries[0].y = __builtin_bswap16(hdrMetadata.displayPrimaries[1].y);
- mdcv.primaries[1].x = __builtin_bswap16(hdrMetadata.displayPrimaries[2].x);
- mdcv.primaries[1].y = __builtin_bswap16(hdrMetadata.displayPrimaries[2].y);
- mdcv.primaries[2].x = __builtin_bswap16(hdrMetadata.displayPrimaries[0].x);
- mdcv.primaries[2].y = __builtin_bswap16(hdrMetadata.displayPrimaries[0].y);
-
- mdcv.white_point.x = __builtin_bswap16(hdrMetadata.whitePoint.x);
- mdcv.white_point.y = __builtin_bswap16(hdrMetadata.whitePoint.y);
-
- // These luminance values are in 10000ths of a nit
- mdcv.luminance_max = __builtin_bswap32((uint32_t)hdrMetadata.maxDisplayLuminance * 10000);
- mdcv.luminance_min = __builtin_bswap32(hdrMetadata.minDisplayLuminance);
-
- NSData* newMdcv = [NSData dataWithBytes:&mdcv length:sizeof(mdcv)];
- if (masteringDisplayColorVolume == nil || ![newMdcv isEqualToData:masteringDisplayColorVolume]) {
- masteringDisplayColorVolume = newMdcv;
- metadataChanged = YES;
- }
- }
- else if (masteringDisplayColorVolume != nil) {
- masteringDisplayColorVolume = nil;
- metadataChanged = YES;
- }
-
- if (hasMetadata && hdrMetadata.maxContentLightLevel != 0 && hdrMetadata.maxFrameAverageLightLevel != 0) {
- // This data is all in big-endian
- struct {
- uint16_t max_content_light_level;
- uint16_t max_frame_average_light_level;
- } __attribute__((packed, aligned(2))) cll;
-
- cll.max_content_light_level = __builtin_bswap16(hdrMetadata.maxContentLightLevel);
- cll.max_frame_average_light_level = __builtin_bswap16(hdrMetadata.maxFrameAverageLightLevel);
-
- NSData* newCll = [NSData dataWithBytes:&cll length:sizeof(cll)];
- if (contentLightLevelInfo == nil || ![newCll isEqualToData:contentLightLevelInfo]) {
- contentLightLevelInfo = newCll;
- metadataChanged = YES;
- }
- }
- else if (contentLightLevelInfo != nil) {
- contentLightLevelInfo = nil;
- metadataChanged = YES;
- }
-
- // If the metadata changed, request an IDR frame to re-create the CMVideoFormatDescription
- if (metadataChanged) {
- LiRequestIdrFrame();
- }
-}
-
@end
diff --git a/Limelight/UIAppView.h b/Limelight/UIAppView.h
index 9f2504c..31108c3 100644
--- a/Limelight/UIAppView.h
+++ b/Limelight/UIAppView.h
@@ -11,16 +11,11 @@
@protocol AppCallback
-- (void) appClicked:(TemporaryApp*)app view:(UIView*)view;
-- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
+- (void) appClicked:(TemporaryApp*) app;
@end
-#if !TARGET_OS_TV
-@interface UIAppView : UIButton
-#else
-@interface UIAppView : UIButton
-#endif
+@interface UIAppView : UIView
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id)callback;
- (void) updateAppImage;
diff --git a/Limelight/UIAppView.m b/Limelight/UIAppView.m
index 6ad9cf4..9a006ef 100644
--- a/Limelight/UIAppView.m
+++ b/Limelight/UIAppView.m
@@ -7,15 +7,12 @@
//
#import "UIAppView.h"
-#import "AppAssetManager.h"
-
-static const float REFRESH_CYCLE = 1.0f;
@implementation UIAppView {
TemporaryApp* _app;
+ UIButton* _appButton;
UILabel* _appLabel;
UIImageView* _appOverlay;
- UIImageView* _appImage;
NSCache* _artCache;
id _callback;
}
@@ -33,121 +30,35 @@ static UIImage* noImage;
if (noImage == nil) {
noImage = [UIImage imageNamed:@"NoAppImage"];
}
-
-#if TARGET_OS_TV
- self.frame = CGRectMake(0, 0, 200, 265);
-#else
- self.frame = CGRectMake(0, 0, 150, 200);
-#endif
- [self setAlpha:app.hidden ? 0.4 : 1.0];
+ _appButton = [UIButton buttonWithType:UIButtonTypeCustom];
+ [_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
+ [_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
+ [_appButton sizeToFit];
+ [_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
- _appImage = [[UIImageView alloc] initWithFrame:self.frame];
- [_appImage setImage:noImage];
- [self addSubview:_appImage];
+ [self addSubview:_appButton];
+ [self sizeToFit];
- // Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
- // for tvOS devices and iOS prior to 13.0.
-#if !TARGET_OS_TV
- if (@available(iOS 13.0, *)) {
- UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
- [self addInteraction:rightClickInteraction];
- }
- else
-#endif
- {
- UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(appLongClicked:)];
- [self addGestureRecognizer:longPressRecognizer];
- }
+ _appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
+ self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
- [self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
-
- [self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
- [self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
-
-#if TARGET_OS_TV
- _appImage.adjustsImageWhenAncestorFocused = YES;
-#else
// Rasterizing the cell layer increases rendering performance by quite a bit
- // but we want it unrasterized for tvOS where it must be scaled.
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
- if (@available(iOS 13.4.1, *)) {
- // Allow the button style to change when moused over
- self.pointerInteractionEnabled = YES;
- }
-#endif
-
- [self updateAppImage];
-
return self;
}
-- (void)didMoveToSuperview {
- // Start our update loop when we are added to our cell
- if (self.superview != nil) {
- [self updateLoop];
- }
+- (void) appClicked {
+ [_callback appClicked:_app];
}
-- (void) appClicked:(UIView *)view {
- [_callback appClicked:_app view:view];
-}
-
-- (void) appLongClicked:(UILongPressGestureRecognizer*)gesture {
- if (gesture.state == UIGestureRecognizerStateBegan) {
- [_callback appLongClicked:_app view:self];
- }
-}
-
-#if !TARGET_OS_TV
-- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
- configurationForMenuAtLocation:(CGPoint)location {
- // We don't want to trigger the primary action at this point, so cancel
- // tracking touch on this view now. This will also have the (intended)
- // effect of removing the touch highlight on this view.
- [self cancelTrackingWithEvent:nil];
-
- [_callback appLongClicked:_app view:self];
- return nil;
-}
-#endif
-
- (void) updateAppImage {
if (_appOverlay != nil) {
[_appOverlay removeFromSuperview];
_appOverlay = nil;
}
- if (_appLabel != nil) {
- [_appLabel removeFromSuperview];
- _appLabel = nil;
- }
-
- BOOL noAppImage = false;
-
- // First check the memory cache
- UIImage* appImage = [_artCache objectForKey:_app];
- if (appImage == nil) {
- // Next try to load from the on disk cache
- appImage = [UIImage imageWithContentsOfFile:[AppAssetManager boxArtPathForApp:_app]];
- if (appImage != nil) {
- [_artCache setObject:appImage forKey:_app];
- }
- }
-
- if (appImage != nil) {
- // This size of image might be blank image received from GameStream.
- // TODO: Improve no-app image detection
- if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
- !(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
- [_appImage setImage:appImage];
- } else {
- noAppImage = true;
- }
- } else {
- noAppImage = true;
- }
if ([_app.id isEqualToString:_app.host.currentGame]) {
// Only create the app overlay if needed
@@ -155,83 +66,57 @@ static UIImage* noImage;
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
_appOverlay.layer.shadowOpacity = 1;
- _appOverlay.layer.shadowRadius = 4.0;
- _appOverlay.contentMode = UIViewContentModeScaleAspectFit;
+ _appOverlay.layer.shadowRadius = 2.0;
+
+ [self addSubview:_appOverlay];
+
+ _appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f);
+ [_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
+ }
+
+ // TODO: Improve no-app image detection
+ BOOL noAppImage = false;
+
+ if (_app.image != nil) {
+ // Load the decoded image from the cache
+ UIImage* appImage = [_artCache objectForKey:_app];
+ if (appImage == nil) {
+ // Not cached; we have to decode this now
+ appImage = [UIImage imageWithData:_app.image];
+ [_artCache setObject:appImage forKey:_app];
+ }
+
+ // This size of image might be blank image received from GameStream.
+ if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
+ !(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
+ _appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
+ self.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
+ _appOverlay.frame = CGRectMake(0, 0, self.frame.size.width / 2.f, self.frame.size.height / 4.f);
+ _appOverlay.layer.shadowRadius = 4.0;
+ [_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
+ [_appButton setBackgroundImage:appImage forState:UIControlStateNormal];
+ [self setNeedsDisplay];
+ } else {
+ noAppImage = true;
+ }
+ } else {
+ noAppImage = true;
}
if (noAppImage) {
_appLabel = [[UILabel alloc] init];
+ CGFloat padding = 4.f;
+ [_appLabel setFrame: CGRectMake(padding, padding, _appButton.frame.size.width - 2 * padding, _appButton.frame.size.height - 2 * padding)];
[_appLabel setTextColor:[UIColor whiteColor]];
- [_appLabel setText:_app.name];
- [_appLabel setFont:[UIFont systemFontOfSize:24]];
+ [_appLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:10.f]];
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
[_appLabel setTextAlignment:NSTextAlignmentCenter];
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
[_appLabel setNumberOfLines:0];
+ [_appLabel setText:_app.name];
+ [_appButton addSubview:_appLabel];
}
- [self positionSubviews];
-
-#if TARGET_OS_TV
- [_appImage.overlayContentView addSubview:_appLabel];
- [_appImage.overlayContentView addSubview:_appOverlay];
-#else
- [self addSubview:_appLabel];
- [self addSubview:_appOverlay];
-#endif
-}
-
-- (void) buttonSelected:(id)sender {
- _appImage.layer.opacity = 0.5f;
-}
-- (void) buttonDeselected:(id)sender {
- _appImage.layer.opacity = 1.0f;
-}
-
-- (void) positionSubviews {
- CGFloat padding = 5.f;
- CGSize frameSize = _appImage.frame.size;
- CGPoint center = _appImage.center;
-
- if (_appLabel != nil) {
- if (_appOverlay != nil) {
- _appOverlay.frame = CGRectMake(0, 0, frameSize.width / 3, frameSize.width / 3);
- _appOverlay.center = CGPointMake(frameSize.width / 2, padding + _appOverlay.frame.size.height / 2);
-
- [_appLabel setFrame:CGRectMake(padding, _appOverlay.frame.size.height + padding, frameSize.width - 2 * padding, frameSize.height - _appOverlay.frame.size.height - 2 * padding)];
- }
- else {
- [_appLabel setFrame:CGRectMake(padding, padding, frameSize.width - 2 * padding, frameSize.height - 2 * padding)];
- }
- }
- else if (_appOverlay != nil) {
- _appOverlay.frame = CGRectMake(0, 0, frameSize.width / 2, frameSize.width / 2);
- _appOverlay.center = center;
- }
-}
-
-- (void) updateLoop {
- // Stop immediately if the view has been detached
- if (self.superview == nil) {
- return;
- }
-
- // Update the app image if neccessary
- if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
- (_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
- [self updateAppImage];
- }
-
- // Show no shadow for hidden apps. Because we adjust the opacity of the
- // cells for hidden apps, it makes them look bad when the shadow draws
- // through the app tile.
- self.superview.layer.shadowOpacity = _app.hidden ? 0.0f : 0.5f;
-
- // Update opacity if neccessary
- [self setAlpha:_app.hidden ? 0.4 : 1.0];
-
- // Queue the next refresh cycle
- [self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
@end
diff --git a/Limelight/UIComputerView.h b/Limelight/UIComputerView.h
index 911c699..7bbf698 100644
--- a/Limelight/UIComputerView.h
+++ b/Limelight/UIComputerView.h
@@ -17,11 +17,7 @@
@end
-#if !TARGET_OS_TV
-@interface UIComputerView : UIButton
-#else
-@interface UIComputerView : UIButton
-#endif
+@interface UIComputerView : UIView
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id)callback;
- (id) initForAddWithCallback:(id)callback;
diff --git a/Limelight/UIComputerView.m b/Limelight/UIComputerView.m
index 450ce3b..364610c 100644
--- a/Limelight/UIComputerView.m
+++ b/Limelight/UIComputerView.m
@@ -10,107 +10,65 @@
@implementation UIComputerView {
TemporaryHost* _host;
- UIImageView* _hostIcon;
+ UIButton* _hostButton;
UILabel* _hostLabel;
- UIImageView* _hostOverlay;
- UIActivityIndicatorView* _hostSpinner;
+ UILabel* _hostStatus;
+ UILabel* _hostPairState;
id _callback;
CGSize _labelSize;
}
static const float REFRESH_CYCLE = 2.0f;
-
-#if TARGET_OS_TV
-static const int ITEM_PADDING = 50;
-static const int LABEL_DY = 40;
-#else
-static const int ITEM_PADDING = 0;
static const int LABEL_DY = 20;
-#endif
- (id) init {
self = [super init];
-
-#if TARGET_OS_TV
- self.frame = CGRectMake(0, 0, 400, 400);
-#else
- if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
- self.frame = CGRectMake(0, 0, 200, 200);
- } else {
- self.frame = CGRectMake(0, 0, 100, 100);
- }
-#endif
+ _hostButton = [UIButton buttonWithType:UIButtonTypeCustom];
+ [_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
+ [_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
+ [_hostButton sizeToFit];
- _hostIcon = [[UIImageView alloc] initWithFrame:self.frame];
- [_hostIcon setImage:[UIImage imageNamed:@"Computer"]];
-
- self.layer.shadowColor = [[UIColor blackColor] CGColor];
- self.layer.shadowOffset = CGSizeMake(5,8);
- self.layer.shadowOpacity = 0.3;
-
- [self addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
- [self addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
+ _hostButton.layer.shadowColor = [[UIColor blackColor] CGColor];
+ _hostButton.layer.shadowOffset = CGSizeMake(5,8);
+ _hostButton.layer.shadowOpacity = 0.7;
_hostLabel = [[UILabel alloc] init];
- _hostLabel.textColor = [UIColor whiteColor];
-
- _hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(self.frame.size.width / 3, _hostIcon.frame.size.height / 4, _hostIcon.frame.size.width / 3, self.frame.size.height / 3)];
- _hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
- [_hostSpinner setFrame:_hostOverlay.frame];
- _hostSpinner.userInteractionEnabled = NO;
- _hostSpinner.hidesWhenStopped = YES;
-
- [self addSubview:_hostLabel];
- [self addSubview:_hostIcon];
-
-#if TARGET_OS_TV
- _hostIcon.clipsToBounds = NO;
- _hostIcon.adjustsImageWhenAncestorFocused = YES;
- _hostIcon.masksFocusEffectToContents = YES;
-
- self.adjustsImageWhenHighlighted = NO;
-
- _hostOverlay.masksFocusEffectToContents = YES;
- _hostOverlay.adjustsImageWhenAncestorFocused = NO;
-
- [_hostIcon.overlayContentView addSubview:_hostOverlay];
- [_hostIcon.overlayContentView addSubview:_hostSpinner];
-#else
- [self addSubview:_hostOverlay];
- [self addSubview:_hostSpinner];
-
- if (@available(iOS 13.4.1, *)) {
- // Allow the button style to change when moused over
- self.pointerInteractionEnabled = YES;
- }
-#endif
-
+ _hostStatus = [[UILabel alloc] init];
+ _hostPairState = [[UILabel alloc] init];
+ [_hostLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
+ [_hostStatus setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
+ [_hostPairState setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
return self;
}
-- (void) hostButtonSelected:(id)sender {
- _hostIcon.layer.opacity = 0.5f;
- _hostSpinner.layer.opacity = 0.5f;
- _hostOverlay.layer.opacity = 0.5f;
-}
-- (void) hostButtonDeselected:(id)sender {
- _hostIcon.layer.opacity = 1.0f;
- _hostSpinner.layer.opacity = 1.0f;
- _hostOverlay.layer.opacity = 1.0f;
-}
-
- (id) initForAddWithCallback:(id)callback {
self = [self init];
_callback = callback;
- [self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
+ [_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
+ [_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
+ [_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
+ [_hostButton sizeToFit];
- [_hostLabel setText:@"Add Host Manually"];
+ [_hostLabel setText:@"Add Host"];
[_hostLabel sizeToFit];
+ _hostLabel.textColor = [UIColor whiteColor];
+ _hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
- [_hostOverlay setImage:[UIImage imageNamed:@"AddOverlayIcon"]];
+ UIImageView* addIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AddComputerIcon"]];
+ [addIcon sizeToFit];
+ addIcon.center = CGPointMake(_hostButton.frame.origin.x + _hostButton.frame.size.width, _hostButton.frame.origin.y);
+
+ // This is required to ensure this button is the same size as the others
+ _hostPairState.text = @"None";
+ _hostStatus.text = @"None";
+ [_hostPairState sizeToFit];
+ [_hostStatus sizeToFit];
[self updateBounds];
-
+ [self addSubview:_hostButton];
+ [self addSubview:_hostLabel];
+ [self addSubview:addIcon];
+
return self;
}
@@ -119,114 +77,98 @@ static const int LABEL_DY = 20;
_host = host;
_callback = callback;
- // Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
- // for tvOS devices and iOS prior to 13.0.
-#if !TARGET_OS_TV
- if (@available(iOS 13.0, *)) {
- UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
- [self addInteraction:rightClickInteraction];
- }
- else
-#endif
- {
- UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
- [self addGestureRecognizer:longPressRecognizer];
- }
-
- [self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
+ UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked)];
+ [_hostButton addGestureRecognizer:longPressRecognizer];
+ [_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
[self updateContentsForHost:host];
+ [self updateBounds];
+ [self addSubview:_hostButton];
+ [self addSubview:_hostLabel];
+ [self addSubview:_hostStatus];
+ [self addSubview:_hostPairState];
+ [self startUpdateLoop];
return self;
}
-- (void)didMoveToSuperview {
- // Start our update loop when we are added to our cell
- if (self.superview != nil && _host != nil) {
- [self updateLoop];
- }
-}
-
- (void) updateBounds {
float x = FLT_MAX;
float y = FLT_MAX;
float width = 0;
float height;
- float iconX = _hostIcon.frame.origin.x + _hostIcon.frame.size.width / 2;
- _hostLabel.center = CGPointMake(iconX, _hostIcon.frame.origin.y + _hostIcon.frame.size.height + LABEL_DY);
-
- x = MIN(x, _hostIcon.frame.origin.x);
+ x = MIN(x, _hostButton.frame.origin.x);
x = MIN(x, _hostLabel.frame.origin.x);
+ x = MIN(x, _hostStatus.frame.origin.x);
+ x = MIN(x, _hostPairState.frame.origin.x);
- y = MIN(y, _hostIcon.frame.origin.y);
+ y = MIN(y, _hostButton.frame.origin.y);
y = MIN(y, _hostLabel.frame.origin.y);
+ y = MIN(y, _hostStatus.frame.origin.y);
+ y = MIN(y, _hostPairState.frame.origin.y);
- width = MAX(width, _hostIcon.frame.size.width);
+ width = MAX(width, _hostButton.frame.size.width);
width = MAX(width, _hostLabel.frame.size.width);
+ width = MAX(width, _hostStatus.frame.size.width);
+ width = MAX(width, _hostPairState.frame.size.width);
- height = _hostIcon.frame.size.height +
+ height = _hostButton.frame.size.height +
_hostLabel.frame.size.height +
+ _hostStatus.frame.size.height +
+ _hostPairState.frame.size.height +
LABEL_DY / 2;
- self.bounds = CGRectMake(x - ITEM_PADDING, y - ITEM_PADDING, width + 2 * ITEM_PADDING, height + 2 * ITEM_PADDING);
+ self.bounds = CGRectMake(x, y, width, height);
+ self.frame = CGRectMake(x, y, width, height);
}
- (void) updateContentsForHost:(TemporaryHost*)host {
_hostLabel.text = _host.name;
+ _hostLabel.textColor = [UIColor whiteColor];
[_hostLabel sizeToFit];
- if (host.state == StateOnline) {
- [_hostSpinner stopAnimating];
-
- if (host.pairState == PairStateUnpaired) {
- [_hostOverlay setImage:[UIImage imageNamed:@"LockedOverlayIcon"]];
- }
- else {
- [_hostOverlay setImage:nil];
- }
- }
- else if (host.state == StateOffline) {
- [_hostSpinner stopAnimating];
- [_hostOverlay setImage:[UIImage imageNamed:@"ErrorOverlayIcon"]];
- }
- else {
- [_hostSpinner startAnimating];
+ switch (host.pairState) {
+ case PairStateUnknown:
+ _hostPairState.text = @"Pair State Unknown";
+ break;
+ case PairStateUnpaired:
+ _hostPairState.text = @"Not Paired";
+ break;
+ case PairStatePaired:
+ _hostPairState.text = @"Paired";
+ break;
}
+ _hostPairState.textColor = [UIColor whiteColor];
+ [_hostPairState sizeToFit];
- [self updateBounds];
+ if (host.online) {
+ _hostStatus.text = @"Online";
+ _hostStatus.textColor = [UIColor greenColor];
+ } else {
+ _hostStatus.text = @"Offline";
+ _hostStatus.textColor = [UIColor grayColor];
+ }
+ [_hostStatus sizeToFit];
+
+ float x = _hostButton.frame.origin.x + _hostButton.frame.size.width / 2;
+ _hostLabel.center = CGPointMake(x, _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
+ _hostPairState.center = CGPointMake(x, _hostLabel.center.y + LABEL_DY);
+ _hostStatus.center = CGPointMake(x, _hostPairState.center.y + LABEL_DY);
}
-- (void) updateLoop {
- // Stop immediately if the view has been detached
- if (self.superview == nil) {
- return;
- }
-
- [self updateContentsForHost:_host];
-
- // Queue the next refresh cycle
+- (void) startUpdateLoop {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
-- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
- if (gesture.state == UIGestureRecognizerStateBegan) {
- [_callback hostLongClicked:_host view:self];
- }
+- (void) updateLoop {
+ [self updateContentsForHost:_host];
+ [self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
-#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];
-
+- (void) hostLongClicked {
[_callback hostLongClicked:_host view:self];
- return nil;
}
-#endif
- (void) hostClicked {
[_callback hostClicked:_host view:self];
diff --git a/Limelight/Utility/Logger.m b/Limelight/Utility/Logger.m
index 3d1d004..f676313 100644
--- a/Limelight/Utility/Logger.m
+++ b/Limelight/Utility/Logger.m
@@ -8,8 +8,6 @@
#import "Logger.h"
-static LogLevel LoggerLogLevel = LOG_I;
-
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args);
void Log(LogLevel level, NSString* fmt, ...) {
@@ -29,10 +27,6 @@ void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...) {
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
NSString* levelPrefix = @"";
- if (level < LoggerLogLevel) {
- return;
- }
-
switch(level) {
case LOG_D:
levelPrefix = PRFX_DEBUG;
@@ -47,8 +41,6 @@ void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
levelPrefix = PRFX_ERROR;
break;
default:
- levelPrefix = @"";
- assert(false);
break;
}
NSString* prefixedString;
diff --git a/Limelight/Utility/Utils.h b/Limelight/Utility/Utils.h
index 1616bf9..2ec0ab1 100644
--- a/Limelight/Utility/Utils.h
+++ b/Limelight/Utility/Utils.h
@@ -6,6 +6,8 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
+#import
+
@interface Utils : NSObject
typedef NS_ENUM(int, PairState) {
@@ -14,27 +16,12 @@ typedef NS_ENUM(int, PairState) {
PairStatePaired
};
-typedef NS_ENUM(int, State) {
- StateUnknown,
- StateOffline,
- StateOnline
-};
-
FOUNDATION_EXPORT NSString *const deviceName;
+ (NSData*) randomBytes:(NSInteger)length;
+ (NSString*) bytesToHex:(NSData*)data;
+ (NSData*) hexToBytes:(NSString*) hex;
-+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
-+ (BOOL) isActiveNetworkVPN;
-+ (BOOL) parseAddressPortString:(NSString*)addressPort address:(NSRange*)address port:(NSRange*)port;
-+ (NSString*) addressPortStringToAddress:(NSString*)addressPort;
-+ (unsigned short) addressPortStringToPort:(NSString*)addressPort;
-+ (NSString*) addressAndPortToAddressPortString:(NSString*)address port:(unsigned short)port;
-
-#if !TARGET_OS_TV
-+ (void) launchUrl:(NSString*)urlString;
-#endif
++ (int) resolveHost:(NSString*)host;
@end
@@ -42,4 +29,4 @@ FOUNDATION_EXPORT NSString *const deviceName;
- (NSString*) trim;
-@end
+@end
\ No newline at end of file
diff --git a/Limelight/Utility/Utils.m b/Limelight/Utility/Utils.m
index fded11b..3ce858c 100644
--- a/Limelight/Utility/Utils.m
+++ b/Limelight/Utility/Utils.m
@@ -50,100 +50,27 @@ NSString *const deviceName = @"roth";
return hex;
}
-+ (BOOL)isActiveNetworkVPN {
- NSDictionary *dict = CFBridgingRelease(CFNetworkCopySystemProxySettings());
- NSArray *keys = [dict[@"__SCOPED__"] allKeys];
- for (NSString *key in keys) {
- if ([key containsString:@"tap"] ||
- [key containsString:@"tun"] ||
- [key containsString:@"ppp"] ||
- [key containsString:@"ipsec"]) {
- return YES;
++ (int) resolveHost:(NSString*)host {
+ struct hostent *hostent;
+
+ if (inet_addr([host UTF8String]) != INADDR_NONE) {
+ // Already an IP address
+ int addr = inet_addr([host UTF8String]);
+ Log(LOG_I, @"host address: %d", addr);
+ return addr;
+ } else {
+ hostent = gethostbyname([host UTF8String]);
+ if (hostent != NULL) {
+ char* ipstr = inet_ntoa(*(struct in_addr*)hostent->h_addr_list[0]);
+ Log(LOG_I, @"Resolved %@ -> %s", host, ipstr);
+ int addr = inet_addr(ipstr);
+ Log(LOG_I, @"host address: %d", addr);
+ return addr;
+ } else {
+ Log(LOG_W, @"Failed to resolve host: %d", h_errno);
+ return 0;
}
}
- return NO;
-}
-
-#if !TARGET_OS_TV
-+ (void) launchUrl:(NSString*)urlString {
- [[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil];
-}
-#endif
-
-+ (void) addHelpOptionToDialog:(UIAlertController*)dialog {
-#if !TARGET_OS_TV
- // tvOS doesn't have a browser
- [dialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- [Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"];
- }]];
-#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
diff --git a/Limelight/ViewControllers/LoadingFrameViewController.h b/Limelight/ViewControllers/LoadingFrameViewController.h
index eb48071..0635725 100644
--- a/Limelight/ViewControllers/LoadingFrameViewController.h
+++ b/Limelight/ViewControllers/LoadingFrameViewController.h
@@ -10,11 +10,5 @@
@interface LoadingFrameViewController : UIViewController
-- (void)showLoadingFrame:(void (^)(void))completion;
-
-- (void)dismissLoadingFrame:(void (^)(void))completion;
-
-- (BOOL)isShown;
-
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingSpinner;
@end
diff --git a/Limelight/ViewControllers/LoadingFrameViewController.m b/Limelight/ViewControllers/LoadingFrameViewController.m
index 550ea60..340464e 100644
--- a/Limelight/ViewControllers/LoadingFrameViewController.m
+++ b/Limelight/ViewControllers/LoadingFrameViewController.m
@@ -8,9 +8,11 @@
#import "LoadingFrameViewController.h"
-@implementation LoadingFrameViewController {
- BOOL presented;
-};
+@interface LoadingFrameViewController ()
+
+@end
+
+@implementation LoadingFrameViewController
- (void)viewDidLoad {
[super viewDidLoad];
@@ -18,66 +20,19 @@
self.loadingSpinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
}
-- (UIViewController*) activeViewController {
- UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
-
- while (topController.presentedViewController) {
- topController = topController.presentedViewController;
- }
-
- return topController;
+- (void)didReceiveMemoryWarning {
+ [super didReceiveMemoryWarning];
+ // Dispose of any resources that can be recreated.
}
-- (void)showLoadingFrame:(void (^)(void))completion {
- if (!presented) {
- Log(LOG_I, @"Loading frame presenting start");
- presented = YES;
- [[self activeViewController] presentViewController:self animated:NO completion:^{
- Log(LOG_I, @"Loading frame presenting complete");
- if (completion) {
- completion();
- }
- }];
- }
- else if (completion) {
- Log(LOG_E, @"Loading frame already shown!");
- completion();
- }
-}
+/*
+#pragma mark - Navigation
-- (void)dismissLoadingFrame:(void (^)(void))completion {
- if (presented) {
- Log(LOG_I, @"Loading frame hiding start");
- [self dismissViewControllerAnimated:NO completion:^{
- Log(LOG_I, @"Loading frame hiding complete");
-
- // Since presented is set to NO here rather than
- // immediately in dismissLoadingFrame, we may
- // falsely avoid displaying the loading frame if
- // a dismiss is in progress while attempting to show
- // the frame. That's preferable to crashing due to
- // displaying the same VC twice though.
- //
- // This scenario can happen if the app is suspended
- // while the dismiss is in progress then on resume
- // it attempts to display it again before the dismiss
- // completes. It can be reproduced by rapidly pressing
- // Home and switching back to Moonlight while in the app grid.
- // It reproduces more easily if the VC transitions are animated.
- self->presented = NO;
-
- if (completion) {
- completion();
- }
- }];
- }
- else if (completion) {
- completion();
- }
-}
-
-- (BOOL)isShown {
- return presented;
+// In a storyboard-based application, you will often want to do a little preparation before navigation
+- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
+ // Get the new view controller using [segue destinationViewController].
+ // Pass the selected object to the new view controller.
}
+*/
@end
diff --git a/Limelight/ViewControllers/MainFrameViewController.h b/Limelight/ViewControllers/MainFrameViewController.h
index 5461ec9..1348b12 100644
--- a/Limelight/ViewControllers/MainFrameViewController.h
+++ b/Limelight/ViewControllers/MainFrameViewController.h
@@ -17,9 +17,7 @@
@interface MainFrameViewController : UICollectionViewController
-@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsButton;
-#if !TARGET_OS_TV
-@property (weak, nonatomic) IBOutlet UIBarButtonItem *upButton;
-#endif
+@property (strong, nonatomic) IBOutlet UIButton *limelightLogoButton;
+@property (weak, nonatomic) IBOutlet UIBarButtonItem *computerNameButton;
@end
diff --git a/Limelight/ViewControllers/MainFrameViewController.m b/Limelight/ViewControllers/MainFrameViewController.m
index 169bb13..07c7afc 100644
--- a/Limelight/ViewControllers/MainFrameViewController.m
+++ b/Limelight/ViewControllers/MainFrameViewController.m
@@ -15,6 +15,7 @@
#import "Utils.h"
#import "UIComputerView.h"
#import "UIAppView.h"
+#import "SettingsViewController.h"
#import "DataManager.h"
#import "TemporarySettings.h"
#import "WakeOnLanManager.h"
@@ -25,226 +26,158 @@
#import "ComputerScrollView.h"
#import "TemporaryApp.h"
#import "IdManager.h"
-#import "ConnectionHelper.h"
-
-#if !TARGET_OS_TV
-#import "SettingsViewController.h"
-#else
-#import
-#endif
-
-#import
-
-#include
@implementation MainFrameViewController {
NSOperationQueue* _opQueue;
TemporaryHost* _selectedHost;
- BOOL _showHiddenApps;
NSString* _uniqueId;
- NSData* _clientCert;
+ NSData* _cert;
DiscoveryManager* _discMan;
AppAssetManager* _appManager;
StreamConfiguration* _streamConfig;
UIAlertController* _pairAlert;
- LoadingFrameViewController* _loadingFrame;
UIScrollView* hostScrollView;
- FrontViewPosition currentPosition;
+ int currentPosition;
NSArray* _sortedAppList;
NSCache* _boxArtCache;
- bool _background;
-#if TARGET_OS_TV
- UITapGestureRecognizer* _menuRecognizer;
-#endif
}
static NSMutableSet* hostList;
-- (void)startPairing:(NSString *)PIN {
- // Needs to be synchronous to ensure the alert is shown before any potential
- // failure callback could be invoked.
- dispatch_sync(dispatch_get_main_queue(), ^{
- self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
- message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@\n\nIf your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.", PIN]
- preferredStyle:UIAlertControllerStyleAlert];
- [self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
- self->_pairAlert = nil;
- [self->_discMan startDiscovery];
- [self hideLoadingFrame: ^{
- [self showHostSelectionView];
- }];
+- (void)showPIN:(NSString *)PIN {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ _pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
+ message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]
+ preferredStyle:UIAlertControllerStyleAlert];
+ [_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
+ [_discMan startDiscovery];
+ [self hideLoadingFrame];
}]];
- [[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil];
+ [self presentViewController:_pairAlert animated:YES completion:nil];
});
}
-- (void)displayPairingFailureDialog:(NSString *)message {
- UIAlertController* failedDialog = [UIAlertController alertControllerWithTitle:@"Pairing Failed"
- message:message
- preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:failedDialog];
- [failedDialog addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
-
- [_discMan startDiscovery];
-
- [self hideLoadingFrame: ^{
- [self showHostSelectionView];
- [[self activeViewController] presentViewController:failedDialog animated:YES completion:nil];
- }];
-}
-
- (void)pairFailed:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
- if (self->_pairAlert != nil) {
- [self->_pairAlert dismissViewControllerAnimated:YES completion:^{
- [self displayPairingFailureDialog:message];
- }];
- self->_pairAlert = nil;
- }
+ [_pairAlert dismissViewControllerAnimated:YES completion:^{
+ _pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing Failed"
+ message:message
+ preferredStyle:UIAlertControllerStyleAlert];
+ [_pairAlert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
+ [self presentViewController:_pairAlert animated:YES completion:nil];
+
+ [_discMan startDiscovery];
+ [self hideLoadingFrame];
+ }];
});
}
-- (void)pairSuccessful:(NSData*)serverCert {
+- (void)pairSuccessful {
dispatch_async(dispatch_get_main_queue(), ^{
- // Store the cert from pairing with the host
- self->_selectedHost.serverCert = serverCert;
-
- [self->_pairAlert dismissViewControllerAnimated:YES completion:nil];
- self->_pairAlert = nil;
-
- [self->_discMan startDiscovery];
+ [_pairAlert dismissViewControllerAnimated:YES completion:nil];
+ [_discMan startDiscovery];
[self alreadyPaired];
});
}
-- (void)disableUpButton {
-#if !TARGET_OS_TV
- [self->_upButton setTitle:nil];
-#endif
-}
-
-- (void)enableUpButton {
-#if !TARGET_OS_TV
- [self->_upButton setTitle:@"Select New Host"];
-#endif
-}
-
-- (void)updateTitle {
- if (_selectedHost != nil) {
- self.title = _selectedHost.name;
- }
- else if ([hostList count] == 0) {
- self.title = @"Searching for PCs on your network...";
- }
- else {
- self.title = @"Select Host";
- }
-}
-
- (void)alreadyPaired {
BOOL usingCachedAppList = false;
// Capture the host here because it can change once we
// leave the main thread
TemporaryHost* host = _selectedHost;
- if (host == nil) {
- [self hideLoadingFrame: nil];
- return;
- }
if ([host.appList count] > 0) {
usingCachedAppList = true;
dispatch_async(dispatch_get_main_queue(), ^{
- if (host != self->_selectedHost) {
- [self hideLoadingFrame: nil];
+ if (host != _selectedHost) {
return;
}
+ _computerNameButton.title = host.name;
+ [self.navigationController.navigationBar setNeedsLayout];
+
[self updateAppsForHost:host];
- [self hideLoadingFrame: nil];
+ [self hideLoadingFrame];
});
}
Log(LOG_I, @"Using cached app list: %d", usingCachedAppList);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:_uniqueId deviceName:deviceName cert:_cert];
+
// Exempt this host from discovery while handling the applist query
- [self->_discMan pauseDiscoveryForHost:host];
+ [_discMan removeHostFromDiscovery:host];
- AppListResponse* appListResp = [ConnectionHelper getAppListForHost:host];
+ // Try up to 5 times to get the app list
+ AppListResponse* appListResp;
+ for (int i = 0; i < 5; i++) {
+ appListResp = [[AppListResponse alloc] init];
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:appListResp withUrlRequest:[hMan newAppListRequest]]];
+ if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
+ Log(LOG_W, @"Failed to get applist on try %d: %@", i, appListResp.statusMessage);
+
+ // Wait for one second then retry
+ [NSThread sleepForTimeInterval:1];
+ }
+ else {
+ Log(LOG_I, @"App list successfully retreived - took %d tries", i);
+ break;
+ }
+ }
- [self->_discMan resumeDiscoveryForHost:host];
+ [_discMan addHostToDiscovery:host];
- if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
+ if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
Log(LOG_W, @"Failed to get applist: %@", appListResp.statusMessage);
dispatch_async(dispatch_get_main_queue(), ^{
- if (host != self->_selectedHost) {
- [self hideLoadingFrame: nil];
+ [self hideLoadingFrame];
+
+ if (host != _selectedHost) {
return;
}
- UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Interrupted"
- message:appListResp.statusMessage
+ UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching App List Failed"
+ message:@"The connection to the PC was interrupted."
preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:applistAlert];
- [applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- [self hideLoadingFrame: ^{
- [self showHostSelectionView];
- [[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
- }];
- host.state = StateOffline;
+ [applistAlert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
+ [self presentViewController:applistAlert animated:YES completion:nil];
+ host.online = NO;
+ [self showHostSelectionView];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateApplist:[appListResp getAppList] forHost:host];
- if (host != self->_selectedHost) {
- [self hideLoadingFrame: nil];
+ if (host != _selectedHost) {
return;
}
+ _computerNameButton.title = host.name;
+ [self.navigationController.navigationBar setNeedsLayout];
+
[self updateAppsForHost:host];
- [self->_appManager stopRetrieving];
- [self->_appManager retrieveAssetsFromHost:host];
- [self hideLoadingFrame: nil];
+ [_appManager stopRetrieving];
+ [_appManager retrieveAssetsFromHost:host];
+ [self hideLoadingFrame];
});
}
});
}
-- (void) updateAppEntry:(TemporaryApp*)app forHost:(TemporaryHost*)host {
- DataManager* database = [[DataManager alloc] init];
- NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
-
- for (TemporaryApp* savedApp in newHostAppList) {
- if ([app.id isEqualToString:savedApp.id]) {
- savedApp.name = app.name;
- savedApp.hdrSupported = app.hdrSupported;
- savedApp.hidden = app.hidden;
-
- host.appList = newHostAppList;
-
- [database updateAppsForExistingHost:host];
- return;
- }
- }
-}
-
- (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host {
DataManager* database = [[DataManager alloc] init];
- NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
for (TemporaryApp* app in newList) {
BOOL appAlreadyInList = NO;
- for (TemporaryApp* savedApp in newHostAppList) {
+ for (TemporaryApp* savedApp in host.appList) {
if ([app.id isEqualToString:savedApp.id]) {
savedApp.name = app.name;
- savedApp.hdrSupported = app.hdrSupported;
- // Don't propagate hidden, because we want the local data to prevail
appAlreadyInList = YES;
break;
}
}
if (!appAlreadyInList) {
app.host = host;
- [newHostAppList addObject:app];
+ [host.appList addObject:app];
}
}
@@ -252,7 +185,7 @@ static NSMutableSet* hostList;
do {
appWasRemoved = NO;
- for (TemporaryApp* app in newHostAppList) {
+ for (TemporaryApp* app in host.appList) {
appWasRemoved = YES;
for (TemporaryApp* mergedApp in newList) {
if ([mergedApp.id isEqualToString:app.id]) {
@@ -264,7 +197,7 @@ static NSMutableSet* hostList;
// Removing the app mutates the list we're iterating (which isn't legal).
// We need to jump out of this loop and restart enumeration.
- [newHostAppList removeObject:app];
+ [host.appList removeObject:app];
// It's important to remove the app record from the database
// since we'll have a constraint violation now that appList
@@ -277,31 +210,14 @@ static NSMutableSet* hostList;
// Keep looping until the list is no longer being mutated
} while (appWasRemoved);
-
- host.appList = newHostAppList;
[database updateAppsForExistingHost:host];
-
- // This host may be eligible for a shortcut now that the app list
- // has been populated
- [self updateHostShortcuts];
}
- (void)showHostSelectionView {
-#if TARGET_OS_TV
- // Remove the menu button intercept to allow the app to exit
- // when at the host selection view.
- [self.navigationController.view removeGestureRecognizer:_menuRecognizer];
-#endif
-
[_appManager stopRetrieving];
- _showHiddenApps = NO;
_selectedHost = nil;
- _sortedAppList = nil;
-
- [self updateTitle];
- [self disableUpButton];
-
+ _computerNameButton.title = @"No Host Selected";
[self.collectionView reloadData];
[self.view addSubview:hostScrollView];
}
@@ -311,6 +227,9 @@ static NSMutableSet* hostList;
// on the main thread
[self updateBoxArtCacheForApp:app];
+ DataManager* dataManager = [[DataManager alloc] init];
+ [dataManager updateIconForExistingApp: app];
+
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
@@ -320,9 +239,8 @@ static NSMutableSet* hostList;
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Network Error"
message:@"Failed to resolve host."
preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:alert];
- [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- [[self activeViewController] presentViewController:alert animated:YES completion:nil];
+ [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
+ [self presentViewController:alert animated:YES completion:nil];
}
- (void) hostClicked:(TemporaryHost *)host view:(UIView *)view {
@@ -330,218 +248,89 @@ static NSMutableSet* hostList;
// This shows the context menu with wake, delete, etc. rather
// than just hanging for a while and failing as we would in this
// code path.
- if (host.state != StateOnline && view != nil) {
+ if (!host.online && view != nil) {
[self hostLongClicked:host view:view];
return;
}
Log(LOG_D, @"Clicked host: %@", host.name);
_selectedHost = host;
- [self updateTitle];
- [self enableUpButton];
[self disableNavigation];
-#if TARGET_OS_TV
- // Intercept the menu key to go back to the host page
- [self.navigationController.view addGestureRecognizer:_menuRecognizer];
-#endif
-
// If we are online, paired, and have a cached app list, skip straight
// to the app grid without a loading frame. This is the fast path that users
- // should hit most. Check for a valid view because we don't want to hit the fast
- // path after coming back from streaming, since we need to fetch serverinfo too
- // so that our active game data is correct.
- if (host.state == StateOnline && host.pairState == PairStatePaired && host.appList.count > 0 && view != nil) {
+ // should hit most.
+ if (host.online && host.pairState == PairStatePaired && host.appList.count > 0) {
[self alreadyPaired];
return;
}
- [self showLoadingFrame: ^{
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- // Wait for the PC's status to be known
- while (host.state == StateUnknown) {
- sleep(1);
- }
-
- // Don't bother polling if the server is already offline
- if (host.state == StateOffline) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideLoadingFrame:^{
- [self showHostSelectionView];
- }];
- });
- return;
- }
-
- HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
- ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
-
- // Exempt this host from discovery while handling the serverinfo request
- [self->_discMan pauseDiscoveryForHost:host];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
- fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
- [self->_discMan resumeDiscoveryForHost:host];
-
- if (![serverInfoResp isStatusOk]) {
- Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
- dispatch_async(dispatch_get_main_queue(), ^{
- if (host != self->_selectedHost) {
- [self hideLoadingFrame:nil];
- return;
- }
-
- UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
- message:serverInfoResp.statusMessage
- preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:applistAlert];
- [applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
-
- // Only display an alert if this was the result of a real
- // user action, not just passively entering the foreground again
- [self hideLoadingFrame: ^{
- [self showHostSelectionView];
- if (view != nil) {
- [[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
- }
- }];
-
- host.state = StateOffline;
- });
+ [self showLoadingFrame];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:_uniqueId deviceName:deviceName cert:_cert];
+ ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
+
+ // Exempt this host from discovery while handling the serverinfo request
+ [_discMan removeHostFromDiscovery:host];
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest]
+ fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
+ [_discMan addHostToDiscovery:host];
+
+ if (serverInfoResp == nil || ![serverInfoResp isStatusOk]) {
+ Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self hideLoadingFrame];
+
+ if (host != _selectedHost) {
+ return;
+ }
+
+ UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching Server Info Failed"
+ message:@"The connection to the PC was interrupted."
+ preferredStyle:UIAlertControllerStyleAlert];
+ [applistAlert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
+ [self presentViewController:applistAlert animated:YES completion:nil];
+ host.online = NO;
+ [self showHostSelectionView];
+ });
+ } else {
+ Log(LOG_D, @"server info pair status: %@", [serverInfoResp getStringTag:@"PairStatus"]);
+ if ([[serverInfoResp getStringTag:@"PairStatus"] isEqualToString:@"1"]) {
+ Log(LOG_I, @"Already Paired");
+ [self alreadyPaired];
} else {
- // Update the host object with this data
- [serverInfoResp populateHost:host];
- if (host.pairState == PairStatePaired) {
- Log(LOG_I, @"Already Paired");
- [self alreadyPaired];
- }
- // Only pair when this was the result of explicit user action
- else if (view != nil) {
- Log(LOG_I, @"Trying to pair");
- // Polling the server while pairing causes the server to screw up
- [self->_discMan stopDiscoveryBlocking];
- PairManager* pMan = [[PairManager alloc] initWithManager:hMan clientCert:self->_clientCert callback:self];
- [self->_opQueue addOperation:pMan];
- }
- else {
- // Not user action, so just return to host screen
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideLoadingFrame:^{
- [self showHostSelectionView];
- }];
- });
- }
+ Log(LOG_I, @"Trying to pair");
+ // Polling the server while pairing causes the server to screw up
+ [_discMan stopDiscoveryBlocking];
+ PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:_cert callback:self];
+ [_opQueue addOperation:pMan];
}
- });
- }];
-}
-
-- (UIViewController*) activeViewController {
- UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
-
- while (topController.presentedViewController) {
- topController = topController.presentedViewController;
- }
-
- return topController;
+ }
+ });
}
- (void)hostLongClicked:(TemporaryHost *)host view:(UIView *)view {
Log(LOG_D, @"Long clicked host: %@", host.name);
- NSString* message;
-
- switch (host.state) {
- case StateOffline:
- message = @"Offline";
- break;
-
- case StateOnline:
- if (host.pairState == PairStatePaired) {
- message = @"Online - Paired";
- }
- else {
- message = @"Online - Not Paired";
- }
- break;
-
- case StateUnknown:
- message = @"Connecting";
- break;
-
- default:
- break;
- }
-
- UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:message preferredStyle:UIAlertControllerStyleActionSheet];
- if (host.state != StateOnline) {
- [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake PC" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake-On-LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert];
+ UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:@"" preferredStyle:UIAlertControllerStyleActionSheet];
+ if (!host.online) {
+ [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
+ UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake On Lan" message:@"" preferredStyle:UIAlertControllerStyleAlert];
[wolAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- if (host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"]) {
+ if (host.pairState != PairStatePaired) {
+ wolAlert.message = @"Cannot wake host because you are not paired";
+ } else if (host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"]) {
wolAlert.message = @"Host MAC unknown, unable to send WOL Packet";
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[WakeOnLanManager wakeHost:host];
});
- wolAlert.message = @"Successfully sent wake-up request. It may take a few moments for the PC to wake. If it never wakes up, ensure it's properly configured for Wake-on-LAN.";
+ wolAlert.message = @"Sent WOL Packet";
}
- [[self activeViewController] presentViewController:wolAlert animated:YES completion:nil];
+ [self presentViewController:wolAlert animated:YES completion:nil];
}]];
}
- else if (host.pairState == PairStatePaired) {
- [longClickAlert addAction:[UIAlertAction actionWithTitle:@"View All Apps" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- self->_showHiddenApps = YES;
- [self hostClicked:host view:view];
- }]];
-
-#if !TARGET_OS_TV
- if (host.isNvidiaServerSoftware) {
- [longClickAlert addAction:[UIAlertAction actionWithTitle:@"NVIDIA GameStream End-of-Service" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- [Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ"];
- }]];
- }
-#endif
- }
- [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Test Network" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) {
- [self showLoadingFrame:^{
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- // Perform the network test on a GCD worker thread. It may take a while.
- unsigned int portTestResult = LiTestClientConnectivity(CONN_TEST_SERVER, 443, ML_PORT_FLAG_ALL);
- dispatch_sync(dispatch_get_main_queue(), ^{
- [self hideLoadingFrame:^{
- NSString* message;
-
- if (portTestResult == 0) {
- message = @"This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nVisit the Moonlight Setup Guide on GitHub for additional setup help and troubleshooting steps.";
- }
- else if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
- message = @"The network test could not be performed because none of Moonlight's connection testing servers were reachable. Check your Internet connection or try again later.";
- }
- else {
- char blockedPorts[512];
- LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
- message = [NSString stringWithFormat:@"Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n%s", blockedPorts];
- }
-
- UIAlertController* netTestAlert = [UIAlertController alertControllerWithTitle:@"Network Test Complete" message:message preferredStyle:UIAlertControllerStyleAlert];
- [netTestAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- [[self activeViewController] presentViewController:netTestAlert animated:YES completion:nil];
- }];
- });
- });
- }];
- }]];
-#if !TARGET_OS_TV
- if (host.state != StateOnline) {
- [longClickAlert addAction:[UIAlertAction actionWithTitle:@"NVIDIA GameStream End-of-Service" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- [Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ"];
- }]];
- [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- [Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"];
- }]];
- }
-#endif
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Remove Host" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
- [self->_discMan removeHostFromDiscovery:host];
+ [_discMan removeHostFromDiscovery:host];
DataManager* dataMan = [[DataManager alloc] init];
[dataMan removeHost:host];
@synchronized(hostList) {
@@ -556,307 +345,134 @@ static NSMutableSet* hostList;
longClickAlert.popoverPresentationController.sourceView = view;
longClickAlert.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view
- [[self activeViewController] presentViewController:longClickAlert animated:YES completion:nil];
+ [self presentViewController:longClickAlert animated:YES completion:^{
+ [self updateHosts];
+ }];
}
- (void) addHostClicked {
Log(LOG_D, @"Clicked add host");
- 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];
+ [self showLoadingFrame];
+ UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Host Address" message:@"Please enter a hostname or IP address" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- NSString* hostAddress = [((UITextField*)[[alertController textFields] objectAtIndex:0]).text trim];
- [self showLoadingFrame:^{
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- [self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
- if (host != nil) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideLoadingFrame:^{
- @synchronized(hostList) {
- [hostList addObject:host];
- }
- [self updateHosts];
- }];
- });
- } else {
- unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443,
- ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
- if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
- error = [error stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
+ NSString* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+ [_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
+ if (host != nil) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ @synchronized(hostList) {
+ [hostList addObject:host];
}
-
- UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:error preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:hostNotFoundAlert];
- [hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideLoadingFrame:^{
- [[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil];
- }];
- });
- }
- }];
- });
- }];
+ [self updateHosts];
+ });
+ } else {
+ UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host" message:error preferredStyle:UIAlertControllerStyleAlert];
+ [hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self presentViewController:hostNotFoundAlert animated:YES completion:nil];
+ });
+ }
+ }];});
}]];
[alertController addTextFieldWithConfigurationHandler:nil];
- [[self activeViewController] presentViewController:alertController animated:YES completion:nil];
+ [self hideLoadingFrame];
+ [self presentViewController:alertController animated:YES completion:nil];
}
-- (void) prepareToStreamApp:(TemporaryApp *)app {
+- (void) appClicked:(TemporaryApp *)app {
+ Log(LOG_D, @"Clicked app: %@", app.name);
_streamConfig = [[StreamConfiguration alloc] init];
_streamConfig.host = app.host.activeAddress;
- _streamConfig.httpsPort = app.host.httpsPort;
_streamConfig.appID = app.id;
- _streamConfig.appName = app.name;
- _streamConfig.serverCert = app.host.serverCert;
DataManager* dataMan = [[DataManager alloc] init];
TemporarySettings* streamSettings = [dataMan getSettings];
_streamConfig.frameRate = [streamSettings.framerate intValue];
- if (@available(iOS 10.3, *)) {
- // Don't stream more FPS than the display can show
- if (_streamConfig.frameRate > [UIScreen mainScreen].maximumFramesPerSecond) {
- _streamConfig.frameRate = (int)[UIScreen mainScreen].maximumFramesPerSecond;
- Log(LOG_W, @"Clamping FPS to maximum refresh rate: %d", _streamConfig.frameRate);
- }
- }
-
+ _streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.height = [streamSettings.height intValue];
_streamConfig.width = [streamSettings.width intValue];
-#if TARGET_OS_TV
- // Don't allow streaming 4K on the Apple TV HD
- struct utsname systemInfo;
- uname(&systemInfo);
- if (strcmp(systemInfo.machine, "AppleTV5,3") == 0 && _streamConfig.height >= 2160) {
- Log(LOG_W, @"4K streaming not supported on Apple TV HD");
- _streamConfig.width = 1920;
- _streamConfig.height = 1080;
- }
-#endif
-
- _streamConfig.bitRate = [streamSettings.bitrate intValue];
- _streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
- _streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
- _streamConfig.useFramePacing = streamSettings.useFramePacing;
- _streamConfig.swapABXYButtons = streamSettings.swapABXYButtons;
-
- // multiController must be set before calling getConnectedGamepadMask
- _streamConfig.multiController = streamSettings.multiController;
- _streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig];
-
- // Probe for supported channel configurations
- int physicalOutputChannels = (int)[AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
- Log(LOG_I, @"Audio device supports %d channels", physicalOutputChannels);
-
- int numberOfChannels = MIN([streamSettings.audioConfig intValue], physicalOutputChannels);
- Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels);
- if (numberOfChannels >= 8) {
- _streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
- }
- else if (numberOfChannels >= 6) {
- _streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
- }
- else {
- _streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
- }
-
- _streamConfig.serverCodecModeSupport = app.host.serverCodecModeSupport;
-
- switch (streamSettings.preferredCodec) {
- case CODEC_PREF_AV1:
-#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
- if (VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) {
- _streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN8;
- }
-#endif
- // Fall-through
-
- case CODEC_PREF_AUTO:
- case CODEC_PREF_HEVC:
- if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
- _streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265;
- }
- // Fall-through
-
- case CODEC_PREF_H264:
- _streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H264;
- break;
- }
-
- // HEVC is supported if the user wants it (or it's required by the chosen resolution) and the SoC supports it
- if ((_streamConfig.width > 4096 || _streamConfig.height > 4096 || streamSettings.enableHdr) && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
- _streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265;
-
- // HEVC Main10 is supported if the user wants it and the display supports it
- if (streamSettings.enableHdr && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0) {
- _streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265_MAIN10;
- }
- }
-
-#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
- // Add the AV1 Main10 format if AV1 and HDR are both enabled and supported
- if ((_streamConfig.supportedVideoFormats & VIDEO_FORMAT_MASK_AV1) && streamSettings.enableHdr &&
- VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1) && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0) {
- _streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN10;
- }
-#endif
-}
-
-- (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
- Log(LOG_D, @"Long clicked app: %@", app.name);
[_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];
+ [[self revealViewController] revealToggle:self];
}
-#endif
-
+
TemporaryApp* currentApp = [self findRunningApp:app.host];
-
- NSString* message;
-
- if (currentApp == nil || [app.id isEqualToString:currentApp.id]) {
- if (app.hidden) {
- message = @"Hidden";
- }
- else {
- message = @"";
- }
- }
- else {
- message = [NSString stringWithFormat:@"%@ is currently running", currentApp.name];
- }
-
- UIAlertController* alertController = [UIAlertController
- alertControllerWithTitle: app.name
- message:message
- preferredStyle:UIAlertControllerStyleActionSheet];
-
- [alertController addAction:[UIAlertAction
- actionWithTitle:currentApp == nil ? @"Launch App" : ([app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
- if (currentApp != nil) {
- Log(LOG_I, @"Resuming application: %@", currentApp.name);
- [self prepareToStreamApp:currentApp];
- }
- else {
- Log(LOG_I, @"Launching application: %@", app.name);
- [self prepareToStreamApp:app];
- }
-
- [self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
- }]];
-
if (currentApp != nil) {
+ UIAlertController* alertController = [UIAlertController
+ alertControllerWithTitle: app.name
+ message: [app.id isEqualToString:currentApp.id] ? @"" : [NSString stringWithFormat:@"%@ is currently running", currentApp.name]preferredStyle:UIAlertControllerStyleAlert];
+ [alertController addAction:[UIAlertAction
+ actionWithTitle:[app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
+ Log(LOG_I, @"Resuming application: %@", currentApp.name);
+ [self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
+ }]];
[alertController addAction:[UIAlertAction actionWithTitle:
[app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
Log(LOG_I, @"Quitting application: %@", currentApp.name);
- [self showLoadingFrame: ^{
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host];
- HttpResponse* quitResponse = [[HttpResponse alloc] init];
- HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host.activeAddress uniqueId:_uniqueId deviceName:deviceName cert:_cert];
+ HttpResponse* quitResponse = [[HttpResponse alloc] init];
+ HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
+
+ // Exempt this host from discovery while handling the quit operation
+ [_discMan removeHostFromDiscovery:app.host];
+ [hMan executeRequestSynchronously:quitRequest];
+ if (quitResponse.statusCode == 200) {
+ ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
+ [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest]
+ fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
+ if (![serverInfoResp isStatusOk] || ![[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_AVAILABLE"]) {
+ // On newer GFE versions, the quit request succeeds even though the app doesn't
+ // really quit if another client tries to kill your app. We'll patch the response
+ // to look like the old error in that case, so the UI behaves.
+ quitResponse.statusCode = 599;
+ }
+ }
+ [_discMan addHostToDiscovery:app.host];
+
+ UIAlertController* alert;
+
+ // If it fails, display an error and stop the current operation
+ if (quitResponse.statusCode != 200) {
+ alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed"
+ message:@"Failed to quit app. If this app was started by "
+ "another device, you'll need to quit from that device."
+ preferredStyle:UIAlertControllerStyleAlert];
+ }
+ // If it succeeds and we're to start streaming, segue to the stream and return
+ else if (![app.id isEqualToString:currentApp.id]) {
+ app.host.currentGame = @"0";
- // Exempt this host from discovery while handling the quit operation
- [self->_discMan pauseDiscoveryForHost:app.host];
- [hMan executeRequestSynchronously:quitRequest];
- if (quitResponse.statusCode == 200) {
- ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
- [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
- fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
- if (![serverInfoResp isStatusOk] || [[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
- // On newer GFE versions, the quit request succeeds even though the app doesn't
- // really quit if another client tries to kill your app. We'll patch the response
- // to look like the old error in that case, so the UI behaves.
- quitResponse.statusCode = 599;
- }
- else if ([serverInfoResp isStatusOk]) {
- // Update the host object with this info
- [serverInfoResp populateHost:app.host];
- }
- }
- [self->_discMan resumeDiscoveryForHost:app.host];
-
- // If it fails, display an error and stop the current operation
- if (quitResponse.statusCode != 200) {
- UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed"
- message:@"Failed to quit app. If this app was started by "
- "another device, you'll need to quit from that device."
- preferredStyle:UIAlertControllerStyleAlert];
- [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- dispatch_async(dispatch_get_main_queue(), ^{
- [self updateAppsForHost:app.host];
- [self hideLoadingFrame: ^{
- [[self activeViewController] presentViewController:alert animated:YES completion:nil];
- }];
- });
- }
- else {
- app.host.currentGame = @"0";
- dispatch_async(dispatch_get_main_queue(), ^{
- // If it succeeds and we're to start streaming, segue to the stream
- if (![app.id isEqualToString:currentApp.id]) {
- [self prepareToStreamApp:app];
- [self hideLoadingFrame: ^{
- [self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
- }];
- }
- else {
- // Otherwise, just hide the loading icon
- [self hideLoadingFrame:nil];
- }
- });
- }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self updateAppsForHost:app.host];
+ [self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
+ });
+
+ return;
+ }
+ // Otherwise, display a dialog to notify the user that the app was quit
+ else {
+ app.host.currentGame = @"0";
+
+ alert = [UIAlertController alertControllerWithTitle:@"Quitting App"
+ message:@"The app was quit successfully."
+ preferredStyle:UIAlertControllerStyleAlert];
+ }
+
+ [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [self updateAppsForHost:app.host];
+ [self presentViewController:alert 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];
+ [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
+ [self presentViewController:alertController animated:YES completion:nil];
} else {
- [self prepareToStreamApp:app];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}
}
@@ -870,22 +486,13 @@ static NSMutableSet* hostList;
return nil;
}
-#if !TARGET_OS_TV
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
// If we moved back to the center position, we should save the settings
if (position == FrontViewPositionLeft) {
[(SettingsViewController*)[revealController rearViewController] saveSettings];
}
-
currentPosition = position;
}
-#endif
-
-#if TARGET_OS_TV
-- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
- [self appClicked:_sortedAppList[indexPath.row] view:nil];
-}
-#endif
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.destinationViewController isKindOfClass:[StreamFrameViewController class]]) {
@@ -894,44 +501,26 @@ static NSMutableSet* hostList;
}
}
-- (void) showLoadingFrame:(void (^)(void))completion {
- [_loadingFrame showLoadingFrame:completion];
+- (void) showLoadingFrame {
+ LoadingFrameViewController* loadingFrame = [self.storyboard instantiateViewControllerWithIdentifier:@"loadingFrame"];
+ [self.navigationController presentViewController:loadingFrame animated:YES completion:nil];
}
-- (void) hideLoadingFrame:(void (^)(void))completion {
+- (void) hideLoadingFrame {
+ [self dismissViewControllerAnimated:YES completion:nil];
[self enableNavigation];
- [_loadingFrame dismissLoadingFrame:completion];
-}
-
-- (void)adjustScrollViewForSafeArea:(UIScrollView*)view {
- if (@available(iOS 11.0, *)) {
- if (self.view.safeAreaInsets.left >= 20 || self.view.safeAreaInsets.right >= 20) {
- view.contentInset = UIEdgeInsetsMake(0, 20, 0, 20);
- }
- }
-}
-
-// Adjust the subviews for the safe area on the iPhone X.
-- (void)viewSafeAreaInsetsDidChange {
- [super viewSafeAreaInsetsDidChange];
-
- [self adjustScrollViewForSafeArea:self.collectionView];
- [self adjustScrollViewForSafeArea:self->hostScrollView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
-
-#if !TARGET_OS_TV
+
// Set the side bar button action. When it's tapped, it'll show the sidebar.
- [_settingsButton setTarget:self.revealViewController];
- [_settingsButton setAction:@selector(revealToggle:)];
+ [_limelightLogoButton addTarget:self.revealViewController action:@selector(revealToggle:) forControlEvents:UIControlEventTouchDown];
// Set the host name button action. When it's tapped, it'll show the host selection view.
- [_upButton setTarget:self];
- [_upButton setAction:@selector(showHostSelectionView)];
- [self disableUpButton];
+ [_computerNameButton setTarget:self];
+ [_computerNameButton setAction:@selector(showHostSelectionView)];
// Set the gesture
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
@@ -939,34 +528,13 @@ static NSMutableSet* hostList;
// Get callbacks associated with the viewController
[self.revealViewController setDelegate:self];
- // Disable bounce-back on reveal VC otherwise the settings will snap closed
- // if the user drags all the way off the screen opposite the settings pane.
- self.revealViewController.bounceBackOnOverdraw = NO;
-#else
- // The settings button will direct the user into the Settings app on tvOS
- [_settingsButton setTarget:self];
- [_settingsButton setAction:@selector(openTvSettings:)];
-
- // Restore focus on the selected app on view controller pop navigation
- self.restoresFocusAfterTransition = NO;
- self.collectionView.remembersLastFocusedIndexPath = YES;
-
- _menuRecognizer = [[UITapGestureRecognizer alloc] init];
- [_menuRecognizer addTarget:self action: @selector(showHostSelectionView)];
- _menuRecognizer.allowedPressTypes = [[NSArray alloc] initWithObjects:[NSNumber numberWithLong:UIPressTypeMenu], nil];
-
- self.navigationController.navigationBar.titleTextAttributes = [NSDictionary dictionaryWithObject:[UIColor whiteColor] forKey:NSForegroundColorAttributeName];
-#endif
-
- _loadingFrame = [self.storyboard instantiateViewControllerWithIdentifier:@"loadingFrame"];
-
// Set the current position to the center
currentPosition = FrontViewPositionLeft;
// Set up crypto
- [CryptoManager generateKeyPairUsingSSL];
+ [CryptoManager generateKeyPairUsingSSl];
_uniqueId = [IdManager getUniqueId];
- _clientCert = [CryptoManager readCertFromFile];
+ _cert = [CryptoManager readCertFromFile];
_appManager = [[AppAssetManager alloc] initWithCallback:self];
_opQueue = [[NSOperationQueue alloc] init];
@@ -977,129 +545,56 @@ static NSMutableSet* hostList;
}
_boxArtCache = [[NSCache alloc] init];
-
+
+ [self setAutomaticallyAdjustsScrollViewInsets:NO];
+
hostScrollView = [[ComputerScrollView alloc] init];
hostScrollView.frame = CGRectMake(0, self.navigationController.navigationBar.frame.origin.y + self.navigationController.navigationBar.frame.size.height, self.view.frame.size.width, self.view.frame.size.height / 2);
[hostScrollView setShowsHorizontalScrollIndicator:NO];
hostScrollView.delaysContentTouches = NO;
+ UIButton* pullArrow = [[UIButton alloc] init];
+ [pullArrow addTarget:self.revealViewController action:@selector(revealToggle:) forControlEvents:UIControlEventTouchDown];
+ [pullArrow setImage:[UIImage imageNamed:@"PullArrow"] forState:UIControlStateNormal];
+ [pullArrow sizeToFit];
+ pullArrow.frame = CGRectMake(0,
+ self.collectionView.frame.size.height / 6 - pullArrow.frame.size.height / 2 - self.navigationController.navigationBar.frame.size.height,
+ pullArrow.frame.size.width,
+ pullArrow.frame.size.height);
+
self.collectionView.delaysContentTouches = NO;
self.collectionView.allowsMultipleSelection = NO;
-#if !TARGET_OS_TV
self.collectionView.multipleTouchEnabled = NO;
-#else
- // This is the only way to get long press events on a UICollectionViewCell :(
- UILongPressGestureRecognizer* cellLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleCollectionViewLongPress:)];
- cellLongPress.delaysTouchesBegan = YES;
- [self.collectionView addGestureRecognizer:cellLongPress];
-#endif
[self retrieveSavedHosts];
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
-
- if ([hostList count] == 1) {
- [self hostClicked:[hostList anyObject] view:nil];
- }
- else {
- [self updateTitle];
- [self.view addSubview:hostScrollView];
- }
-}
-
-#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];
- }
+ [[NSNotificationCenter defaultCenter] addObserver: self
+ selector: @selector(handleReturnToForeground)
+ name: UIApplicationWillEnterForegroundNotification
+ object: nil];
+
+ [self updateHosts];
+ [self.view addSubview:hostScrollView];
+ [self.view addSubview:pullArrow];
}
-- (void)openTvSettings:(id)sender
+-(void)dealloc
{
- [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
-}
-#endif
-
--(void)beginForegroundRefresh
-{
- if (!_background) {
- // This will kick off box art caching
- [self updateHosts];
-
- // Reset state first so we can rediscover hosts that were deleted before
- [_discMan resetDiscoveryState];
- [_discMan startDiscovery];
-
- // This will refresh the applist when a paired host is selected
- if (_selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
- [self hostClicked:_selectedHost view:nil];
- }
- }
-}
-
--(void)handlePendingShortcutAction
-{
- // Check if we have a pending shortcut action
- AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
- if (delegate.pcUuidToLoad != nil) {
- // Find the host it corresponds to
- TemporaryHost* matchingHost = nil;
- for (TemporaryHost* host in hostList) {
- if ([host.uuid isEqualToString:delegate.pcUuidToLoad]) {
- matchingHost = host;
- break;
- }
- }
-
- // Clear the pending shortcut action
- delegate.pcUuidToLoad = nil;
-
- // Complete the request
- if (delegate.shortcutCompletionHandler != nil) {
- delegate.shortcutCompletionHandler(matchingHost != nil);
- delegate.shortcutCompletionHandler = nil;
- }
-
- if (matchingHost != nil && _selectedHost != matchingHost) {
- // Navigate to the host page
- [self hostClicked:matchingHost view:nil];
- }
- }
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(void)handleReturnToForeground
{
- _background = NO;
-
- [self beginForegroundRefresh];
-
- // Check for a pending shortcut action when returning to foreground
- [self handlePendingShortcutAction];
-}
-
--(void)handleEnterBackground
-{
- _background = YES;
-
- [_discMan stopDiscovery];
+ // This will refresh the applist when a paired host is selected
+ if (_selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
+ [self hostClicked:_selectedHost view:nil];
+ }
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
-
-#if !TARGET_OS_TV
- [[self revealViewController] setPrimaryViewController:self];
-#endif
-
[self.navigationController setNavigationBarHidden:NO animated:YES];
// Hide 1px border line
@@ -1107,52 +602,19 @@ static NSMutableSet* hostList;
[self.navigationController.navigationBar setShadowImage:fakeImage];
[self.navigationController.navigationBar setBackgroundImage:fakeImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault];
- // Check for a pending shortcut action when appearing
- [self handlePendingShortcutAction];
+ [_discMan startDiscovery];
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(handleReturnToForeground)
- name: UIApplicationDidBecomeActiveNotification
- object: nil];
-
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(handleEnterBackground)
- name: UIApplicationWillResignActiveNotification
- object: nil];
-}
-
-- (void)viewWillAppear:(BOOL)animated
-{
- [super viewWillAppear:animated];
-
- // We can get here on home press while streaming
- // since the stream view segues to us just before
- // entering the background. We can't check the app
- // state here (since it's in transition), so we have
- // to use this function that will use our internal
- // state here to determine whether we're foreground.
- //
- // Note that this is neccessary here as we may enter
- // this view via an error dialog from the stream
- // view, so we won't get a return to active notification
- // for that which would normally fire beginForegroundRefresh.
- [self beginForegroundRefresh];
+ [self handleReturnToForeground];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
-
- // when discovery stops, we must create a new instance because
- // you cannot restart an NSOperation when it is finished
+ // when discovery stops, we must create a new instance because you cannot restart an NSOperation when it is finished
[_discMan stopDiscovery];
// Purge the box art cache
[_boxArtCache removeAllObjects];
-
- // Remove our lifetime observers to avoid triggering them
- // while streaming
- [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void) retrieveSavedHosts {
@@ -1172,54 +634,24 @@ static NSMutableSet* hostList;
if (host.activeAddress == nil) {
host.activeAddress = host.address;
}
- if (host.activeAddress == nil) {
- host.activeAddress = host.ipv6Address;
- }
}
}
}
- (void) updateAllHosts:(NSArray *)hosts {
- // We must copy the array here because it could be modified
- // before our main thread dispatch happens.
- NSArray* hostsCopy = [NSArray arrayWithArray:hosts];
dispatch_async(dispatch_get_main_queue(), ^{
Log(LOG_D, @"New host list:");
- for (TemporaryHost* host in hostsCopy) {
- 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);
+ for (TemporaryHost* host in hosts) {
+ Log(LOG_D, @"Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \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.uuid, host.mac, host.pairState, host.online, host.activeAddress);
}
@synchronized(hostList) {
[hostList removeAllObjects];
- [hostList addObjectsFromArray:hostsCopy];
+ [hostList addObjectsFromArray:hosts];
}
[self updateHosts];
});
}
-- (void)updateHostShortcuts {
-#if !TARGET_OS_TV
- NSMutableArray* quickActions = [[NSMutableArray alloc] init];
-
- @synchronized (hostList) {
- for (TemporaryHost* host in hostList) {
- // Pair state may be unknown if we haven't polled it yet, but the app list
- // count will persist from paired PCs
- if ([host.appList count] > 0) {
- UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
- initWithType:@"PC"
- localizedTitle:host.name
- localizedSubtitle:nil
- icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
- userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
- [quickActions addObject: shortcut];
- }
- }
- }
-
- [UIApplication sharedApplication].shortcutItems = quickActions;
-#endif
-}
-
- (void)updateHosts {
Log(LOG_I, @"Updating hosts...");
[[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
@@ -1234,22 +666,9 @@ static NSMutableSet* hostList;
compView.center = CGPointMake([self getCompViewX:compView addComp:addComp prevEdge:prevEdge], hostScrollView.frame.size.height / 2);
prevEdge = compView.frame.origin.x + compView.frame.size.width;
[hostScrollView addSubview:compView];
-
- // Start jobs to decode the box art in advance
- for (TemporaryApp* app in comp.appList) {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
- [self updateBoxArtCacheForApp:app];
- });
- }
}
}
- // Create or delete host shortcuts as needed
- [self updateHostShortcuts];
-
- // Update the title in case we now have a PC
- [self updateTitle];
-
prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge];
addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2);
@@ -1258,51 +677,21 @@ static NSMutableSet* hostList;
}
- (float) getCompViewX:(UIComputerView*)comp addComp:(UIComputerView*)addComp prevEdge:(float)prevEdge {
- float padding;
-
-#if TARGET_OS_TV
- padding = 100;
-#else
- padding = addComp.frame.size.width / 2;
-#endif
-
if (prevEdge == -1) {
- return hostScrollView.frame.origin.x + comp.frame.size.width / 2 + padding;
+ return hostScrollView.frame.origin.x + comp.frame.size.width / 2 + addComp.frame.size.width / 2;
} else {
- return prevEdge + comp.frame.size.width / 2 + padding;
+ return prevEdge + addComp.frame.size.width / 2 + comp.frame.size.width / 2;
}
}
// This function forces immediate decoding of the UIImage, rather
// than the default lazy decoding that results in janky scrolling.
+ (UIImage*) loadBoxArtForCaching:(TemporaryApp*)app {
- UIImage* boxArt;
- NSData* imageData = [NSData dataWithContentsOfFile:[AppAssetManager boxArtPathForApp:app]];
- if (imageData == nil) {
- // No box art on disk
- return nil;
- }
+ CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)app.image, NULL);
+ CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)@{(id)kCGImageSourceShouldCacheImmediately: (id)kCFBooleanTrue});
- CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
- CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil);
-
- size_t width = CGImageGetWidth(cgImage);
- size_t height = CGImageGetHeight(cgImage);
-
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
- CGContextRef imageContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace,
- kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
- CGColorSpaceRelease(colorSpace);
-
- CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), cgImage);
-
- CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);
-
- boxArt = [UIImage imageWithCGImage:outputImage];
-
- CGImageRelease(outputImage);
- CGContextRelease(imageContext);
+ UIImage *boxArt = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
CFRelease(source);
@@ -1311,12 +700,11 @@ static NSMutableSet* hostList;
}
- (void) updateBoxArtCacheForApp:(TemporaryApp*)app {
- if ([_boxArtCache objectForKey:app] == nil) {
- UIImage* image = [MainFrameViewController loadBoxArtForCaching:app];
- if (image != nil) {
- // Add the image to our cache if it was present
- [_boxArtCache setObject:image forKey:app];
- }
+ if (app.image == nil) {
+ [_boxArtCache removeObjectForKey:app];
+ }
+ else if ([_boxArtCache objectForKey:app] == nil) {
+ [_boxArtCache setObject:[MainFrameViewController loadBoxArtForCaching:app] forKey:app];
}
}
@@ -1329,15 +717,32 @@ static NSMutableSet* hostList;
_sortedAppList = [host.appList allObjects];
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
- if (!_showHiddenApps) {
- NSMutableArray* visibleAppList = [NSMutableArray array];
- for (TemporaryApp* app in _sortedAppList) {
- if (!app.hidden) {
- [visibleAppList addObject:app];
- }
+ // Split the sorted array in half to allow 2 jobs to process app assets at once
+ NSArray *firstHalf;
+ NSArray *secondHalf;
+ NSRange range;
+
+ range.location = 0;
+ range.length = [_sortedAppList count] / 2;
+
+ firstHalf = [_sortedAppList subarrayWithRange:range];
+
+ range.location = range.length;
+ range.length = [_sortedAppList count] - range.length;
+
+ secondHalf = [_sortedAppList subarrayWithRange:range];
+
+ // Start 2 jobs
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ for (TemporaryApp* app in firstHalf) {
+ [self updateBoxArtCacheForApp:app];
}
- _sortedAppList = visibleAppList;
- }
+ });
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ for (TemporaryApp* app in secondHalf) {
+ [self updateBoxArtCacheForApp:app];
+ }
+ });
[hostScrollView removeFromSuperview];
[self.collectionView reloadData];
@@ -1348,6 +753,7 @@ static NSMutableSet* hostList;
TemporaryApp* app = _sortedAppList[indexPath.row];
UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self];
+ [appView updateAppImage];
if (appView.bounds.size.width > 10.0) {
CGFloat scale = cell.bounds.size.width / appView.bounds.size.width;
@@ -1357,20 +763,18 @@ static NSMutableSet* hostList;
[cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added
[cell addSubview:appView];
+
- // Shadow opacity is controlled inside UIAppView based on whether the app
- // is hidden or not during the update cycle.
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.bounds];
cell.layer.masksToBounds = NO;
cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f);
+ cell.layer.shadowOpacity = 0.5f;
cell.layer.shadowPath = shadowPath.CGPath;
-#if !TARGET_OS_TV
- cell.layer.borderWidth = 1;
cell.layer.borderColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.3f] CGColor];
+ cell.layer.borderWidth = 1;
cell.exclusiveTouch = YES;
-#endif
return cell;
}
@@ -1380,8 +784,8 @@ static NSMutableSet* hostList;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
- if (_selectedHost != nil && _sortedAppList != nil) {
- return _sortedAppList.count;
+ if (_selectedHost != nil) {
+ return _selectedHost.appList.count;
}
else {
return 0;
@@ -1391,9 +795,7 @@ static NSMutableSet* hostList;
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
-
- // Purge the box art cache on low memory
- [_boxArtCache removeAllObjects];
+ // Dispose of any resources that can be recreated.
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
@@ -1405,36 +807,16 @@ static NSMutableSet* hostList;
return YES;
}
-#if !TARGET_OS_TV
- (BOOL)shouldAutorotate {
return YES;
}
-#endif
- (void) disableNavigation {
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = NO;
- self.navigationController.navigationBar.topItem.leftBarButtonItem.enabled = NO;
}
- (void) enableNavigation {
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = YES;
- self.navigationController.navigationBar.topItem.leftBarButtonItem.enabled = YES;
-}
-
-#if TARGET_OS_TV
-- (BOOL)canBecomeFocused {
- return YES;
-}
-#endif
-
-- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
-
-#if !TARGET_OS_TV
- if (context.nextFocusedView != nil) {
- [context.nextFocusedView setAlpha:0.8];
- }
- [context.previouslyFocusedView setAlpha:1.0];
-#endif
}
@end
diff --git a/Limelight/ViewControllers/SWRevealViewController.h b/Limelight/ViewControllers/SWRevealViewController.h
index 3eea263..279106e 100755
--- a/Limelight/ViewControllers/SWRevealViewController.h
+++ b/Limelight/ViewControllers/SWRevealViewController.h
@@ -180,8 +180,6 @@ typedef NS_ENUM(NSInteger, SWRevealToggleAnimationType)
@interface SWRevealViewController : UIViewController
-- (void)setPrimaryViewController:(UIViewController*)viewController;
-
/* Basic API */
// Object instance init and rear view setting
diff --git a/Limelight/ViewControllers/SWRevealViewController.m b/Limelight/ViewControllers/SWRevealViewController.m
index bf64803..3ed41fd 100755
--- a/Limelight/ViewControllers/SWRevealViewController.m
+++ b/Limelight/ViewControllers/SWRevealViewController.m
@@ -583,7 +583,6 @@ static CGFloat scaledValue( CGFloat v1, CGFloat min2, CGFloat max2, CGFloat min1
FrontViewPosition _panInitialFrontPosition;
NSMutableArray *_animationQueue;
BOOL _userInteractionStore;
- UIViewController *_primaryViewController;
}
const int FrontViewPositionNone = 0xff;
@@ -626,10 +625,10 @@ const int FrontViewPositionNone = 0xff;
_frontViewPosition = FrontViewPositionLeft;
_rearViewPosition = FrontViewPositionLeft;
_rightViewPosition = FrontViewPositionLeft;
- _rearViewRevealWidth = 510.0f;
+ _rearViewRevealWidth = 260.0f;
_rearViewRevealOverdraw = 60.0f;
_rearViewRevealDisplacement = 40.0f;
- _rightViewRevealWidth = 510.0f;
+ _rightViewRevealWidth = 260.0f;
_rightViewRevealOverdraw = 60.0f;
_rightViewRevealDisplacement = 40.0f;
_bounceBackOnOverdraw = YES;
@@ -996,32 +995,16 @@ const int FrontViewPositionNone = 0xff;
- (void)_getRevealWidth:(CGFloat*)pRevealWidth revealOverDraw:(CGFloat*)pRevealOverdraw forSymetry:(int)symetry
{
- if ( symetry < 0 )
- {
- *pRevealWidth = _rightViewRevealWidth;
- *pRevealOverdraw = _rightViewRevealOverdraw;
- }
- else
- {
- *pRevealWidth = _rearViewRevealWidth;
- *pRevealOverdraw = _rearViewRevealOverdraw;
- }
+ if ( symetry < 0 ) *pRevealWidth = _rightViewRevealWidth, *pRevealOverdraw = _rightViewRevealOverdraw;
+ else *pRevealWidth = _rearViewRevealWidth, *pRevealOverdraw = _rearViewRevealOverdraw;
if (*pRevealWidth < 0) *pRevealWidth = _contentView.bounds.size.width + *pRevealWidth;
}
- (void)_getBounceBack:(BOOL*)pBounceBack pStableDrag:(BOOL*)pStableDrag forSymetry:(int)symetry
{
- if ( symetry < 0 )
- {
- *pBounceBack = _bounceBackOnLeftOverdraw;
- *pStableDrag = _stableDragOnLeftOverdraw;
- }
- else
- {
- *pBounceBack = _bounceBackOnOverdraw;
- *pStableDrag = _stableDragOnOverdraw;
- }
+ if ( symetry < 0 ) *pBounceBack = _bounceBackOnLeftOverdraw, *pStableDrag = _stableDragOnLeftOverdraw;
+ else *pBounceBack = _bounceBackOnOverdraw, *pStableDrag = _stableDragOnOverdraw;
}
- (void)_getAdjustedFrontViewPosition:(FrontViewPosition*)frontViewPosition forSymetry:(int)symetry
@@ -1381,55 +1364,27 @@ const int FrontViewPositionNone = 0xff;
_enqueue( [theSelf _performTransitionOperation:operation withViewController:newViewController animated:animated] );
}
-- (void)setPrimaryViewController:(UIViewController*)viewController
-{
- _primaryViewController = viewController;
-
- // These are derived from the primary view controller
- if (@available(iOS 11.0, *)) {
- [self setNeedsUpdateOfHomeIndicatorAutoHidden];
- [self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
- }
- if (@available(iOS 14.0, *)) {
- [self setNeedsUpdateOfPrefersPointerLocked];
- }
-}
-
-- (UIViewController*)childViewControllerForHomeIndicatorAutoHidden
-{
- return _primaryViewController;
-}
-
-- (UIViewController*)childViewControllerForScreenEdgesDeferringSystemGestures
-{
- return _primaryViewController;
-}
-
-- (UIViewController*)childViewControllerForPointerLock
-{
- return _primaryViewController;
-}
#pragma mark Animated view controller deployment and layout
// Primitive method for view controller deployment and animated layout to the given position.
- (void)_setFrontViewPosition:(FrontViewPosition)newPosition withDuration:(NSTimeInterval)duration
{
- void (^rearDeploymentCompletion)(void) = [self _rearViewDeploymentForNewFrontViewPosition:newPosition];
- void (^rightDeploymentCompletion)(void) = [self _rightViewDeploymentForNewFrontViewPosition:newPosition];
- void (^frontDeploymentCompletion)(void) = [self _frontViewDeploymentForNewFrontViewPosition:newPosition];
+ void (^rearDeploymentCompletion)() = [self _rearViewDeploymentForNewFrontViewPosition:newPosition];
+ void (^rightDeploymentCompletion)() = [self _rightViewDeploymentForNewFrontViewPosition:newPosition];
+ void (^frontDeploymentCompletion)() = [self _frontViewDeploymentForNewFrontViewPosition:newPosition];
- void (^animations)(void) = ^(void)
+ void (^animations)() = ^()
{
// Calling this in the animation block causes the status bar to appear/dissapear in sync with our own animation
[self setNeedsStatusBarAppearanceUpdate];
// We call the layoutSubviews method on the contentView view and send a delegate, which will
// occur inside of an animation block if any animated transition is being performed
- [self->_contentView layoutSubviews];
+ [_contentView layoutSubviews];
- if ([self->_delegate respondsToSelector:@selector(revealController:animateToPosition:)])
- [self->_delegate revealController:self animateToPosition:self->_frontViewPosition];
+ if ([_delegate respondsToSelector:@selector(revealController:animateToPosition:)])
+ [_delegate revealController:self animateToPosition:_frontViewPosition];
};
void (^completion)(BOOL) = ^(BOOL finished)
@@ -1472,31 +1427,21 @@ const int FrontViewPositionNone = 0xff;
UIView *view = nil;
if ( operation == SWRevealControllerOperationReplaceRearController )
- {
- old = _rearViewController;
- _rearViewController = new;
- view = _contentView.rearView;
- }
- else if ( operation == SWRevealControllerOperationReplaceFrontController )
- {
- old = _frontViewController;
- _frontViewController = new;
- view = _contentView.frontView;
- }
- else if ( operation == SWRevealControllerOperationReplaceRightController )
- {
- old = _rightViewController;
- _rightViewController = new;
- view = _contentView.rightView;
- }
-
- void (^completion)(void) = [self _transitionFromViewController:old toViewController:new inView:view];
+ old = _rearViewController, _rearViewController = new, view = _contentView.rearView;
- void (^animationCompletion)(void) = ^
+ else if ( operation == SWRevealControllerOperationReplaceFrontController )
+ old = _frontViewController, _frontViewController = new, view = _contentView.frontView;
+
+ else if ( operation == SWRevealControllerOperationReplaceRightController )
+ old = _rightViewController, _rightViewController = new, view = _contentView.rightView;
+
+ void (^completion)() = [self _transitionFromViewController:old toViewController:new inView:view];
+
+ void (^animationCompletion)() = ^
{
completion();
- if ( [self->_delegate respondsToSelector:@selector(revealController:didAddViewController:forOperation:animated:)] )
- [self->_delegate revealController:self didAddViewController:new forOperation:operation animated:animated];
+ if ( [_delegate respondsToSelector:@selector(revealController:didAddViewController:forOperation:animated:)] )
+ [_delegate revealController:self didAddViewController:new forOperation:operation animated:animated];
[self _dequeue];
};
@@ -1554,16 +1499,16 @@ const int FrontViewPositionNone = 0xff;
_frontViewPosition = newPosition;
- void (^deploymentCompletion)(void) =
+ void (^deploymentCompletion)() =
[self _deploymentForViewController:_frontViewController inView:_contentView.frontView appear:appear disappear:disappear];
- void (^completion)(void) = ^(void)
+ void (^completion)() = ^()
{
deploymentCompletion();
if ( positionIsChanging )
{
- if ( [self->_delegate respondsToSelector:@selector(revealController:didMoveToPosition:)] )
- [self->_delegate revealController:self didMoveToPosition:newPosition];
+ if ( [_delegate respondsToSelector:@selector(revealController:didMoveToPosition:)] )
+ [_delegate revealController:self didMoveToPosition:newPosition];
}
};
@@ -1679,11 +1624,11 @@ const int FrontViewPositionNone = 0xff;
if ( toController ) [self addChildViewController:toController];
- void (^deployCompletion)(void) = [self _deployForViewController:toController inView:view];
+ void (^deployCompletion)() = [self _deployForViewController:toController inView:view];
[fromController willMoveToParentViewController:nil];
- void (^undeployCompletion)(void) = [self _undeployForViewController:fromController];
+ void (^undeployCompletion)() = [self _undeployForViewController:fromController];
void (^completionBlock)(void) = ^(void)
{
diff --git a/Limelight/ViewControllers/SettingsViewController.h b/Limelight/ViewControllers/SettingsViewController.h
index 1a79162..5fdb155 100644
--- a/Limelight/ViewControllers/SettingsViewController.h
+++ b/Limelight/ViewControllers/SettingsViewController.h
@@ -14,27 +14,7 @@
@property (strong, nonatomic) IBOutlet UISlider *bitrateSlider;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
-@property (strong, nonatomic) IBOutlet UIView *resolutionDisplayView;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *swapABXYButtonsSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
-@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
-@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
-
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wunguarded-availability"
-
-// This is okay because it's just an enum and access uses @available checks
-@property(nonatomic) UIUserInterfaceStyle overrideUserInterfaceStyle;
-
-#pragma clang diagnostic pop
- (void) saveSettings;
diff --git a/Limelight/ViewControllers/SettingsViewController.m b/Limelight/ViewControllers/SettingsViewController.m
index bbe8eb2..0d81572 100644
--- a/Limelight/ViewControllers/SettingsViewController.m
+++ b/Limelight/ViewControllers/SettingsViewController.m
@@ -10,455 +10,69 @@
#import "TemporarySettings.h"
#import "DataManager.h"
-#import
-#import
+#define BITRATE_INTERVAL 500 // in kbps
@implementation SettingsViewController {
NSInteger _bitrate;
- NSInteger _lastSelectedResolutionIndex;
}
-
-@dynamic overrideUserInterfaceStyle;
-
static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
-static const int bitrateTable[] = {
- 500,
- 1000,
- 1500,
- 2000,
- 2500,
- 3000,
- 4000,
- 5000,
- 6000,
- 7000,
- 8000,
- 9000,
- 10000,
- 12000,
- 15000,
- 18000,
- 20000,
- 30000,
- 40000,
- 50000,
- 60000,
- 70000,
- 80000,
- 100000,
- 120000,
- 150000,
-};
-const int RESOLUTION_TABLE_SIZE = 7;
-const int RESOLUTION_TABLE_CUSTOM_INDEX = RESOLUTION_TABLE_SIZE - 1;
-CGSize resolutionTable[RESOLUTION_TABLE_SIZE];
-
--(int)getSliderValueForBitrate:(NSInteger)bitrate {
- int i;
-
- for (i = 0; i < (sizeof(bitrateTable) / sizeof(*bitrateTable)); i++) {
- if (bitrate <= bitrateTable[i]) {
- return i;
- }
- }
-
- // Return the last entry in the table
- return i - 1;
-}
-
-// This view is rooted at a ScrollView. To make it scrollable,
-// we'll update content size here.
--(void)viewDidLayoutSubviews {
- CGFloat highestViewY = 0;
-
- // Enumerate the scroll view's subviews looking for the
- // highest view Y value to set our scroll view's content
- // size.
- for (UIView* view in self.scrollView.subviews) {
- // UIScrollViews have 2 default child views
- // which represent the horizontal and vertical scrolling
- // indicators. Ignore any views we don't recognize.
- if (![view isKindOfClass:[UILabel class]] &&
- ![view isKindOfClass:[UISegmentedControl class]] &&
- ![view isKindOfClass:[UISlider class]]) {
- continue;
- }
-
- CGFloat currentViewY = view.frame.origin.y + view.frame.size.height;
- if (currentViewY > highestViewY) {
- highestViewY = currentViewY;
- }
- }
-
- // Add a bit of padding so the view doesn't end right at the button of the display
- self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width,
- highestViewY + 20);
-}
-
-// Adjust the subviews for the safe area on the iPhone X.
-- (void)viewSafeAreaInsetsDidChange {
- [super viewSafeAreaInsetsDidChange];
-
- if (@available(iOS 11.0, *)) {
- for (UIView* view in self.view.subviews) {
- // HACK: The official safe area is much too large for our purposes
- // so we'll just use the presence of any safe area to indicate we should
- // pad by 20.
- if (self.view.safeAreaInsets.left >= 20 || self.view.safeAreaInsets.right >= 20) {
- view.frame = CGRectMake(view.frame.origin.x + 20, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
- }
- }
- }
-}
-
-BOOL isCustomResolution(CGSize res) {
- if (res.width == 0 && res.height == 0) {
- return NO;
- }
-
- for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) {
- if (res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) {
- return NO;
- }
- }
-
- return YES;
-}
- (void)viewDidLoad {
[super viewDidLoad];
-
- // Always run settings in dark mode because we want the light fonts
- if (@available(iOS 13.0, tvOS 13.0, *)) {
- self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
- }
DataManager* dataMan = [[DataManager alloc] init];
TemporarySettings* currentSettings = [dataMan getSettings];
- // Ensure we pick a bitrate that falls exactly onto a slider notch
- _bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
-
- // Get the size of the screen with and without safe area insets
- UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
- CGFloat screenScale = window.screen.scale;
- CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
- CGFloat fullScreenWidth = window.frame.size.width * screenScale;
- CGFloat fullScreenHeight = window.frame.size.height * screenScale;
-
- self.resolutionDisplayView.layer.cornerRadius = 10;
- self.resolutionDisplayView.clipsToBounds = YES;
- UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)];
- [self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap];
-
- resolutionTable[0] = CGSizeMake(640, 360);
- resolutionTable[1] = CGSizeMake(1280, 720);
- resolutionTable[2] = CGSizeMake(1920, 1080);
- resolutionTable[3] = CGSizeMake(3840, 2160);
- resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
- resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
- resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
-
- // Don't populate the custom entry unless we have a custom resolution
- if (!isCustomResolution(resolutionTable[6])) {
- resolutionTable[6] = CGSizeMake(0, 0);
+ // Bitrate is persisted in kbps
+ _bitrate = [currentSettings.bitrate integerValue];
+ NSInteger framerate = [currentSettings.framerate integerValue] == 30 ? 0 : 1;
+ NSInteger resolution;
+ if ([currentSettings.height integerValue] == 720) {
+ resolution = 0;
+ } else if ([currentSettings.height integerValue] == 1080) {
+ resolution = 1;
+ } else {
+ resolution = 0;
}
-
- NSInteger framerate;
- switch ([currentSettings.framerate integerValue]) {
- case 30:
- framerate = 0;
- break;
- default:
- case 60:
- framerate = 1;
- break;
- case 120:
- framerate = 2;
- break;
- }
-
- NSInteger resolution = 1;
- for (int i = 0; i < RESOLUTION_TABLE_SIZE; i++) {
- if ((int) resolutionTable[i].height == [currentSettings.height intValue]
- && (int) resolutionTable[i].width == [currentSettings.width intValue]) {
- resolution = i;
- break;
- }
- }
-
- // Only show the 120 FPS option if we have a > 60-ish Hz display
- bool enable120Fps = false;
- if (@available(iOS 10.3, tvOS 10.3, *)) {
- if ([UIScreen mainScreen].maximumFramesPerSecond > 62) {
- enable120Fps = true;
- }
- }
- if (!enable120Fps) {
- [self.framerateSelector removeSegmentAtIndex:2 animated:NO];
- }
-
- // Disable codec selector segments for unsupported codecs
-#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
- if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1))
-#endif
- {
- [self.codecSelector removeSegmentAtIndex:2 animated:NO];
- }
- if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
- [self.codecSelector removeSegmentAtIndex:1 animated:NO];
-
- // Only enable the 4K option for "recent" devices. We'll judge that by whether
- // they support HEVC decoding (A9 or later).
- [self.resolutionSelector setEnabled:NO forSegmentAtIndex:3];
- }
- switch (currentSettings.preferredCodec) {
- case CODEC_PREF_AUTO:
- [self.codecSelector setSelectedSegmentIndex:self.codecSelector.numberOfSegments - 1];
- break;
-
- case CODEC_PREF_AV1:
- [self.codecSelector setSelectedSegmentIndex:2];
- break;
-
- case CODEC_PREF_HEVC:
- [self.codecSelector setSelectedSegmentIndex:1];
- break;
-
- case CODEC_PREF_H264:
- [self.codecSelector setSelectedSegmentIndex:0];
- break;
- }
-
- if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || !(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10)) {
- [self.hdrSelector removeAllSegments];
- [self.hdrSelector insertSegmentWithTitle:@"Unsupported on this device" atIndex:0 animated:NO];
- [self.hdrSelector setEnabled:NO];
- }
- else {
- [self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
- }
-
- [self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
- [self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
- [self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlay ? 1 : 0];
- [self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
- [self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
- [self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
- [self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
- [self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
- [self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
- _lastSelectedResolutionIndex = resolution;
+
[self.resolutionSelector setSelectedSegmentIndex:resolution];
- [self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
+ [self.resolutionSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
[self.framerateSelector setSelectedSegmentIndex:framerate];
- [self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
+ [self.framerateSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
- [self.onscreenControlSelector setEnabled:!currentSettings.absoluteTouchMode];
- [self.bitrateSlider setMinimumValue:0];
- [self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
- [self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
+ [self.bitrateSlider setValue:(_bitrate / BITRATE_INTERVAL) animated:YES];
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
[self updateBitrateText];
- [self updateResolutionDisplayViewText];
}
-- (void) touchModeChanged {
- // Disable on-screen controls in absolute touch mode
- [self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == 0];
-}
-
-- (void) updateBitrate {
- NSInteger fps = [self getChosenFrameRate];
- NSInteger width = [self getChosenStreamWidth];
- NSInteger height = [self getChosenStreamHeight];
+- (void) newResolutionFpsChosen {
+ NSInteger frameRate = [self getChosenFrameRate];
+ NSInteger resHeight = [self getChosenStreamHeight];
NSInteger defaultBitrate;
- // This logic is shamelessly stolen from Moonlight Qt:
- // https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
-
- // Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
- // bitrate increase for frame rate once we get to values that high.
- float frameRateFactor = (fps <= 60 ? fps : (sqrtf(fps / 60.f) * 60.f)) / 30.f;
-
- // TODO: Collect some empirical data to see if these defaults make sense.
- // We're just using the values that the Shield used, as we have for years.
- struct {
- NSInteger pixels;
- int factor;
- } resTable[] = {
- { 640 * 360, 1 },
- { 854 * 480, 2 },
- { 1280 * 720, 5 },
- { 1920 * 1080, 10 },
- { 2560 * 1440, 20 },
- { 3840 * 2160, 40 },
- { -1, -1 }
- };
-
- // Calculate the resolution factor by linear interpolation of the resolution table
- float resolutionFactor;
- NSInteger pixels = width * height;
- for (int i = 0;; i++) {
- if (pixels == resTable[i].pixels) {
- // We can bail immediately for exact matches
- resolutionFactor = resTable[i].factor;
- break;
- }
- else if (pixels < resTable[i].pixels) {
- if (i == 0) {
- // Never go below the lowest resolution entry
- resolutionFactor = resTable[i].factor;
- }
- else {
- // Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1)
- resolutionFactor = ((float)(pixels - resTable[i-1].pixels) / (resTable[i].pixels - resTable[i-1].pixels)) * (resTable[i].factor - resTable[i-1].factor) + resTable[i-1].factor;
- }
- break;
- }
- else if (resTable[i].pixels == -1) {
- // Never go above the highest resolution entry
- resolutionFactor = resTable[i-1].factor;
- break;
- }
+ // 1080p60 is 20 Mbps
+ if (frameRate == 60 && resHeight == 1080) {
+ defaultBitrate = 20000;
}
-
- defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
- _bitrate = MIN(defaultBitrate, 100000);
- [self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
+ // 720p60 and 1080p30 are 10 Mbps
+ else if (frameRate == 60 || resHeight == 1080) {
+ defaultBitrate = 10000;
+ }
+ // 720p30 is 5 Mbps
+ else {
+ defaultBitrate = 5000;
+ }
+
+ _bitrate = defaultBitrate;
+ [self.bitrateSlider setValue:defaultBitrate / BITRATE_INTERVAL animated:YES];
[self updateBitrateText];
}
-- (void) newResolutionChosen {
- BOOL lastSegmentSelected = [self.resolutionSelector selectedSegmentIndex] + 1 == [self.resolutionSelector numberOfSegments];
- if (lastSegmentSelected) {
- [self promptCustomResolutionDialog];
- }
- else {
- [self updateBitrate];
- [self updateResolutionDisplayViewText];
- _lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
- }
-}
-
-- (void) promptCustomResolutionDialog {
- UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Enter Custom Resolution" message:nil preferredStyle:UIAlertControllerStyleAlert];
-
- [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
- textField.placeholder = @"Video Width";
- textField.clearButtonMode = UITextFieldViewModeAlways;
- textField.borderStyle = UITextBorderStyleRoundedRect;
- textField.keyboardType = UIKeyboardTypeNumberPad;
-
- if (resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width == 0) {
- textField.text = @"";
- }
- else {
- textField.text = [NSString stringWithFormat:@"%d", (int) resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width];
- }
- }];
-
- [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
- textField.placeholder = @"Video Height";
- textField.clearButtonMode = UITextFieldViewModeAlways;
- textField.borderStyle = UITextBorderStyleRoundedRect;
- textField.keyboardType = UIKeyboardTypeNumberPad;
-
- if (resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height == 0) {
- textField.text = @"";
- }
- else {
- textField.text = [NSString stringWithFormat:@"%d", (int) resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height];
- }
- }];
-
- [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
- NSArray * textfields = alertController.textFields;
- UITextField *widthField = textfields[0];
- UITextField *heightField = textfields[1];
-
- long width = [widthField.text integerValue];
- long height = [heightField.text integerValue];
- if (width <= 0 || height <= 0) {
- // Restore the previous selection
- [self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
- return;
- }
-
- // H.264 maximum
- int maxResolutionDimension = 4096;
- if (@available(iOS 11.0, tvOS 11.0, *)) {
- if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
- // HEVC maximum
- maxResolutionDimension = 8192;
- }
- }
-
- // Cap to maximum valid dimensions
- width = MIN(width, maxResolutionDimension);
- height = MIN(height, maxResolutionDimension);
-
- // Cap to minimum valid dimensions
- width = MAX(width, 256);
- height = MAX(height, 256);
-
- resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX] = CGSizeMake(width, height);
- [self updateBitrate];
- [self updateResolutionDisplayViewText];
- self->_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
-
- UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Custom Resolution Selected" message: @"Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.\n\nResolutions that are not supported by your client or host PC may cause streaming errors." preferredStyle:UIAlertControllerStyleAlert];
- [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
- [self presentViewController:alertController animated:YES completion:nil];
- }]];
-
- [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
- // Restore the previous selection
- [self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
- }]];
-
- [self presentViewController:alertController animated:YES completion:nil];
-}
-
-- (void)resolutionDisplayViewTapped:(UITapGestureRecognizer *)sender {
- NSURL *url = [NSURL URLWithString:@"https://moonlight-stream.org/custom-resolution"];
- if ([[UIApplication sharedApplication] canOpenURL:url]) {
- [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
- }
-}
-
-- (void) updateResolutionDisplayViewText {
- NSInteger width = [self getChosenStreamWidth];
- NSInteger height = [self getChosenStreamHeight];
- CGFloat viewFrameWidth = self.resolutionDisplayView.frame.size.width;
- CGFloat viewFrameHeight = self.resolutionDisplayView.frame.size.height;
- CGFloat padding = 10;
- CGFloat fontSize = [UIFont smallSystemFontSize];
-
- for (UIView *subview in self.resolutionDisplayView.subviews) {
- [subview removeFromSuperview];
- }
- UILabel *label1 = [[UILabel alloc] init];
- label1.text = @"Set PC/Game resolution: ";
- label1.font = [UIFont systemFontOfSize:fontSize];
- [label1 sizeToFit];
- label1.frame = CGRectMake(padding, (viewFrameHeight - label1.frame.size.height) / 2, label1.frame.size.width, label1.frame.size.height);
-
- UILabel *label2 = [[UILabel alloc] init];
- label2.text = [NSString stringWithFormat:@"%ld x %ld", (long)width, (long)height];
- [label2 sizeToFit];
- label2.frame = CGRectMake(viewFrameWidth - label2.frame.size.width - padding, (viewFrameHeight - label2.frame.size.height) / 2, label2.frame.size.width, label2.frame.size.height);
-
- [self.resolutionDisplayView addSubview:label1];
- [self.resolutionDisplayView addSubview:label2];
-}
-
- (void) bitrateSliderMoved {
- assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
- _bitrate = bitrateTable[(int)self.bitrateSlider.value];
+ _bitrate = BITRATE_INTERVAL * (int)self.bitrateSlider.value;
[self updateBitrateText];
}
@@ -468,58 +82,15 @@ BOOL isCustomResolution(CGSize res) {
}
- (NSInteger) getChosenFrameRate {
- switch ([self.framerateSelector selectedSegmentIndex]) {
- case 0:
- return 30;
- case 1:
- return 60;
- case 2:
- return 120;
- default:
- abort();
- }
-}
-
-- (uint32_t) getChosenCodecPreference {
- // Auto is always the last segment
- if (self.codecSelector.selectedSegmentIndex == self.codecSelector.numberOfSegments - 1) {
- return CODEC_PREF_AUTO;
- }
- else {
- switch (self.codecSelector.selectedSegmentIndex) {
- case 0:
- return CODEC_PREF_H264;
-
- case 1:
- return CODEC_PREF_HEVC;
-
- case 2:
- return CODEC_PREF_AV1;
-
- default:
- abort();
- }
- }
+ return [self.framerateSelector selectedSegmentIndex] == 0 ? 30 : 60;
}
- (NSInteger) getChosenStreamHeight {
- // because the 4k resolution can be removed
- BOOL lastSegmentSelected = [self.resolutionSelector selectedSegmentIndex] + 1 == [self.resolutionSelector numberOfSegments];
- if (lastSegmentSelected) {
- return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height;
- }
-
- return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].height;
+ return [self.resolutionSelector selectedSegmentIndex] == 0 ? 720 : 1080;
}
- (NSInteger) getChosenStreamWidth {
- // because the 4k resolution can be removed
- BOOL lastSegmentSelected = [self.resolutionSelector selectedSegmentIndex] + 1 == [self.resolutionSelector numberOfSegments];
- if (lastSegmentSelected) {
- return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
- }
-
- return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width;
+ return [self getChosenStreamHeight] == 720 ? 1280 : 1920;
}
- (void) saveSettings {
@@ -528,32 +99,7 @@ BOOL isCustomResolution(CGSize res) {
NSInteger height = [self getChosenStreamHeight];
NSInteger width = [self getChosenStreamWidth];
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
- BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
- BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
- BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
- BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
- uint32_t preferredCodec = [self getChosenCodecPreference];
- BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
- BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
- BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
- BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
- BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
- [dataMan saveSettingsWithBitrate:_bitrate
- framerate:framerate
- height:height
- width:width
- audioConfig:2 // Stereo
- onscreenControls:onscreenControls
- optimizeGames:optimizeGames
- multiController:multiController
- swapABXYButtons:swapABXYButtons
- audioOnPC:audioOnPC
- preferredCodec:preferredCodec
- useFramePacing:useFramePacing
- enableHdr:enableHdr
- btMouseSupport:btMouseSupport
- absoluteTouchMode:absoluteTouchMode
- statsOverlay:statsOverlay];
+ [dataMan saveSettingsWithBitrate:_bitrate framerate:framerate height:height width:width onscreenControls:onscreenControls];
}
- (void)didReceiveMemoryWarning {
diff --git a/Limelight/ViewControllers/StreamFrameViewController.h b/Limelight/ViewControllers/StreamFrameViewController.h
index dc9e5a8..f9bf163 100644
--- a/Limelight/ViewControllers/StreamFrameViewController.h
+++ b/Limelight/ViewControllers/StreamFrameViewController.h
@@ -12,15 +12,9 @@
#import
-#if TARGET_OS_TV
-@import GameController;
-
-@interface StreamFrameViewController : GCEventViewController
-#else
-@interface StreamFrameViewController : UIViewController
-#endif
+@interface StreamFrameViewController : UIViewController
+@property (strong, nonatomic) IBOutlet UILabel *stageLabel;
+@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
@property (nonatomic) StreamConfiguration* streamConfig;
--(void)updatePreferredDisplayMode:(BOOL)streamActive;
-
@end
diff --git a/Limelight/ViewControllers/StreamFrameViewController.m b/Limelight/ViewControllers/StreamFrameViewController.m
index a4c8f34..a6e4e8d 100644
--- a/Limelight/ViewControllers/StreamFrameViewController.m
+++ b/Limelight/ViewControllers/StreamFrameViewController.m
@@ -11,148 +11,32 @@
#import "VideoDecoderRenderer.h"
#import "StreamManager.h"
#import "ControllerSupport.h"
-#import "DataManager.h"
#include
#include
#include
-#include
-
-#if TARGET_OS_TV
-#import
-#import
-#import
-#endif
-
-@interface AVDisplayCriteria()
-@property(readonly) int videoDynamicRange;
-@property(readonly, nonatomic) float refreshRate;
-- (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2;
-@end
@implementation StreamFrameViewController {
ControllerSupport *_controllerSupport;
StreamManager *_streamMan;
- TemporarySettings *_settings;
- NSTimer *_inactivityTimer;
- NSTimer *_statsUpdateTimer;
- UITapGestureRecognizer *_menuTapGestureRecognizer;
- UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
- UITapGestureRecognizer *_playPauseTapGestureRecognizer;
- UITextView *_overlayView;
- UILabel *_stageLabel;
- UILabel *_tipLabel;
- UIActivityIndicatorView *_spinner;
- StreamView *_streamView;
- UIScrollView *_scrollView;
- BOOL _userIsInteracting;
- CGSize _keyboardSize;
-
-#if !TARGET_OS_TV
- UIScreenEdgePanGestureRecognizer *_exitSwipeRecognizer;
-#endif
}
-- (void)viewDidAppear:(BOOL)animated
-{
- [super viewDidAppear:animated];
-
-#if !TARGET_OS_TV
- [[self revealViewController] setPrimaryViewController:self];
-#endif
-}
-
-#if TARGET_OS_TV
-- (void)controllerPauseButtonPressed:(id)sender { }
-- (void)controllerPauseButtonDoublePressed:(id)sender {
- Log(LOG_I, @"Menu double-pressed -- backing out of stream");
- [self returnToMainFrame];
-}
-- (void)controllerPlayPauseButtonPressed:(id)sender {
- Log(LOG_I, @"Play/Pause button pressed -- backing out of stream");
- [self returnToMainFrame];
-}
-#endif
-
-
- (void)viewDidLoad
{
[super viewDidLoad];
[self.navigationController setNavigationBarHidden:YES animated:YES];
+ [self.stageLabel setText:@"Starting App"];
+ [self.stageLabel sizeToFit];
+ self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
+ self.spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - self.stageLabel.frame.size.height - self.spinner.frame.size.height);
[UIApplication sharedApplication].idleTimerDisabled = YES;
- _settings = [[[DataManager alloc] init] getSettings];
-
- _stageLabel = [[UILabel alloc] init];
- [_stageLabel setUserInteractionEnabled:NO];
- [_stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
- [_stageLabel sizeToFit];
- _stageLabel.textAlignment = NSTextAlignmentCenter;
- _stageLabel.textColor = [UIColor whiteColor];
- _stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
-
- _spinner = [[UIActivityIndicatorView alloc] init];
- [_spinner setUserInteractionEnabled:NO];
-#if TARGET_OS_TV
- [_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
-#else
- [_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
-#endif
- [_spinner sizeToFit];
- [_spinner startAnimating];
- _spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - _stageLabel.frame.size.height - _spinner.frame.size.height);
-
- _controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig delegate:self];
- _inactivityTimer = nil;
-
- _streamView = [[StreamView alloc] initWithFrame:self.view.frame];
- [_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig];
-
-#if TARGET_OS_TV
- if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) {
- _menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
- _menuTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
-
- _playPauseTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPlayPauseButtonPressed:)];
- _playPauseTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypePlayPause)];
-
- _menuDoubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonDoublePressed:)];
- _menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2;
- [_menuTapGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
- _menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
- }
-
- [self.view addGestureRecognizer:_menuTapGestureRecognizer];
- [self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer];
- [self.view addGestureRecognizer:_playPauseTapGestureRecognizer];
-
-#else
- _exitSwipeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
- _exitSwipeRecognizer.edges = UIRectEdgeLeft;
- _exitSwipeRecognizer.delaysTouchesBegan = NO;
- _exitSwipeRecognizer.delaysTouchesEnded = NO;
-
- [self.view addGestureRecognizer:_exitSwipeRecognizer];
-#endif
-
- _tipLabel = [[UILabel alloc] init];
- [_tipLabel setUserInteractionEnabled:NO];
-
-#if TARGET_OS_TV
- [_tipLabel setText:@"Tip: Tap the Play/Pause button on the Apple TV Remote to disconnect from your PC"];
-#else
- [_tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
-#endif
-
- [_tipLabel sizeToFit];
- _tipLabel.textColor = [UIColor whiteColor];
- _tipLabel.textAlignment = NSTextAlignmentCenter;
- _tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
+ _controllerSupport = [[ControllerSupport alloc] init];
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
- renderView:_streamView
+ renderView:self.view
connectionCallbacks:self];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:_streamMan];
@@ -161,307 +45,40 @@
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
-
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(applicationDidBecomeActive:)
- name: UIApplicationDidBecomeActiveNotification
- object: nil];
-
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(applicationDidEnterBackground:)
- name: UIApplicationDidEnterBackgroundNotification
- object: nil];
-
-#if 0
- // FIXME: This doesn't work reliably on iPad for some reason. Showing and hiding the keyboard
- // several times in a row will not correctly restore the state of the UIScrollView.
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(keyboardWillShow:)
- name: UIKeyboardWillShowNotification
- object: nil];
-
- [[NSNotificationCenter defaultCenter] addObserver: self
- selector: @selector(keyboardWillHide:)
- name: UIKeyboardWillHideNotification
- object: nil];
-#endif
-
- // Only enable scroll and zoom in absolute touch mode
- if (_settings.absoluteTouchMode) {
- _scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
-#if !TARGET_OS_TV
- [_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2];
-#endif
- [_scrollView setShowsHorizontalScrollIndicator:NO];
- [_scrollView setShowsVerticalScrollIndicator:NO];
- [_scrollView setDelegate:self];
- [_scrollView setMaximumZoomScale:10.0f];
-
- // Add StreamView inside a UIScrollView for absolute mode
- [_scrollView addSubview:_streamView];
- [self.view addSubview:_scrollView];
- }
- else {
- // Add StreamView directly in relative mode
- [self.view addSubview:_streamView];
- }
-
- [self.view addSubview:_stageLabel];
- [self.view addSubview:_spinner];
- [self.view addSubview:_tipLabel];
-}
-
-- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
- return _streamView;
-}
-
-- (void)willMoveToParentViewController:(UIViewController *)parent {
- // Only cleanup when we're being destroyed
- if (parent == nil) {
- [_controllerSupport cleanup];
- [UIApplication sharedApplication].idleTimerDisabled = NO;
- [_streamMan stopStream];
- if (_inactivityTimer != nil) {
- [_inactivityTimer invalidate];
- _inactivityTimer = nil;
- }
- [[NSNotificationCenter defaultCenter] removeObserver:self];
- }
-}
-
-#if 0
-- (void)keyboardWillShow:(NSNotification *)notification {
- _keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
-
- [UIView animateWithDuration:0.3 animations:^{
- CGRect frame = self->_scrollView.frame;
- frame.size.height -= self->_keyboardSize.height;
- self->_scrollView.frame = frame;
- }];
-}
-
--(void)keyboardWillHide:(NSNotification *)notification {
- // NOTE: UIKeyboardFrameEndUserInfoKey returns a different keyboard size
- // than UIKeyboardFrameBeginUserInfoKey, so it's unsuitable for use here
- // to undo the changes made by keyboardWillShow.
-
- [UIView animateWithDuration:0.3 animations:^{
- CGRect frame = self->_scrollView.frame;
- frame.size.height += self->_keyboardSize.height;
- self->_scrollView.frame = frame;
- }];
-}
-#endif
-
-- (void)updateStatsOverlay {
- NSString* overlayText = [self->_streamMan getStatsOverlayText];
-
- dispatch_async(dispatch_get_main_queue(), ^{
- [self updateOverlayText:overlayText];
- });
-}
-
-- (void)updateOverlayText:(NSString*)text {
- if (_overlayView == nil) {
- _overlayView = [[UITextView alloc] init];
-#if !TARGET_OS_TV
- [_overlayView setEditable:NO];
-#endif
- [_overlayView setUserInteractionEnabled:NO];
- [_overlayView setSelectable:NO];
- [_overlayView setScrollEnabled:NO];
-
- // HACK: If not using stats overlay, center the text
- if (_statsUpdateTimer == nil) {
- [_overlayView setTextAlignment:NSTextAlignmentCenter];
- }
-
- [_overlayView setTextColor:[UIColor lightGrayColor]];
- [_overlayView setBackgroundColor:[UIColor blackColor]];
-#if TARGET_OS_TV
- [_overlayView setFont:[UIFont systemFontOfSize:24]];
-#else
- [_overlayView setFont:[UIFont systemFontOfSize:12]];
-#endif
- [_overlayView setAlpha:0.5];
- [self.view addSubview:_overlayView];
- }
-
- if (text != nil) {
- // We set our bounds to the maximum width in order to work around a bug where
- // sizeToFit interacts badly with the UITextView's line breaks, causing the
- // width to get smaller and smaller each time as more line breaks are inserted.
- [_overlayView setBounds:CGRectMake(self.view.frame.origin.x,
- _overlayView.frame.origin.y,
- self.view.frame.size.width,
- _overlayView.frame.size.height)];
- [_overlayView setText:text];
- [_overlayView sizeToFit];
- [_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, _overlayView.frame.size.height / 2)];
- [_overlayView setHidden:NO];
- }
- else {
- [_overlayView setHidden:YES];
- }
}
- (void) returnToMainFrame {
- // Reset display mode back to default
- [self updatePreferredDisplayMode:NO];
-
- [_statsUpdateTimer invalidate];
- _statsUpdateTimer = nil;
-
+ [_controllerSupport cleanup];
[self.navigationController popToRootViewControllerAnimated:YES];
}
-// This will fire if the user opens control center or gets a low battery message
- (void)applicationWillResignActive:(NSNotification *)notification {
- if (_inactivityTimer != nil) {
- [_inactivityTimer invalidate];
- }
-
-#if !TARGET_OS_TV
- // Terminate the stream if the app is inactive for 60 seconds
- Log(LOG_I, @"Starting inactivity termination timer");
- _inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:60
- target:self
- selector:@selector(inactiveTimerExpired:)
- userInfo:nil
- repeats:NO];
-#endif
-}
-
-- (void)inactiveTimerExpired:(NSTimer*)timer {
- Log(LOG_I, @"Terminating stream after inactivity");
-
- [self returnToMainFrame];
-
- _inactivityTimer = nil;
-}
-
-- (void)applicationDidBecomeActive:(NSNotification *)notification {
- // Stop the background timer, since we're foregrounded again
- if (_inactivityTimer != nil) {
- Log(LOG_I, @"Stopping inactivity timer after becoming active again");
- [_inactivityTimer invalidate];
- _inactivityTimer = nil;
- }
-}
-
-// This fires when the home button is pressed
-- (void)applicationDidEnterBackground:(UIApplication *)application {
- Log(LOG_I, @"Terminating stream immediately for backgrounding");
-
- if (_inactivityTimer != nil) {
- [_inactivityTimer invalidate];
- _inactivityTimer = nil;
- }
-
+ [_streamMan stopStream];
[self returnToMainFrame];
}
- (void)edgeSwiped {
- Log(LOG_I, @"User swiped to end stream");
-
+ Log(LOG_D, @"User swiped to end stream");
+ [_streamMan stopStream];
[self returnToMainFrame];
}
- (void) connectionStarted {
Log(LOG_I, @"Connection started");
dispatch_async(dispatch_get_main_queue(), ^{
- // Leave the spinner spinning until it's obscured by
- // the first frame of video.
- self->_stageLabel.hidden = YES;
- self->_tipLabel.hidden = YES;
-
- [self->_streamView showOnScreenControls];
-
- [self->_controllerSupport connectionEstablished];
-
- if (self->_settings.statsOverlay) {
- self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
- target:self
- selector:@selector(updateStatsOverlay)
- userInfo:nil
- repeats:YES];
- }
+ [self.spinner stopAnimating];
+ [self.stageLabel setText:@"Waiting for first frame..."];
+ [self.stageLabel sizeToFit];
+ [(StreamView*)self.view setupOnScreenControls: _controllerSupport swipeDelegate:self];
});
}
-- (void)connectionTerminated:(int)errorCode {
- Log(LOG_I, @"Connection terminated: %d", errorCode);
-
- unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
- unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
+- (void)connectionTerminated:(long)errorCode {
+ Log(LOG_I, @"Connection terminated: %ld", errorCode);
dispatch_async(dispatch_get_main_queue(), ^{
- // Allow the display to go to sleep now
- [UIApplication sharedApplication].idleTimerDisabled = NO;
-
- NSString* title;
- NSString* message;
-
- if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
- title = @"Connection Error";
- message = @"Your device's network connection is blocking Moonlight. Streaming may not work while connected to this network.";
- }
- else {
- switch (errorCode) {
- case ML_ERROR_GRACEFUL_TERMINATION:
- [self returnToMainFrame];
- return;
-
- case ML_ERROR_NO_VIDEO_TRAFFIC:
- title = @"Connection Error";
- message = @"No video received from host.";
- if (portFlags != 0) {
- char failingPorts[256];
- LiStringifyPortFlags(portFlags, "\n", failingPorts, sizeof(failingPorts));
- message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
- }
- break;
-
- case ML_ERROR_NO_VIDEO_FRAME:
- title = @"Connection Error";
- message = @"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.";
- break;
-
- case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
- case ML_ERROR_PROTECTED_CONTENT:
- title = @"Connection Error";
- message = @"Something went wrong on your host PC when starting the stream.\n\nMake sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.";
- break;
-
- case ML_ERROR_FRAME_CONVERSION:
- title = @"Connection Error";
- message = @"The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution.";
- break;
-
- default:
- {
- NSString* errorString;
- if (abs(errorCode) > 1000) {
- // We'll assume large errors are hex values
- errorString = [NSString stringWithFormat:@"%08X", (uint32_t)errorCode];
- }
- else {
- // Smaller values will just be printed as decimal (probably errno.h values)
- errorString = [NSString stringWithFormat:@"%d", errorCode];
- }
-
- title = @"Connection Terminated";
- message = [NSString stringWithFormat: @"The connection was terminated\n\nError code: %@", errorString];
- break;
- }
- }
- }
-
- UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:title
- message:message
- preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:conTermAlert];
- [conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
+ UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:@"Connection Terminated" message:@"The connection was terminated" preferredStyle:UIAlertControllerStyleAlert];
+ [conTermAlert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:conTermAlert animated:YES completion:nil];
@@ -475,39 +92,24 @@
dispatch_async(dispatch_get_main_queue(), ^{
NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName];
NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]];
- [self->_stageLabel setText:titleCase];
- [self->_stageLabel sizeToFit];
- self->_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self->_stageLabel.center.y);
+ [self.stageLabel setText:titleCase];
+ [self.stageLabel sizeToFit];
+ self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y);
});
}
- (void) stageComplete:(const char*)stageName {
}
-- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
- Log(LOG_I, @"Stage %s failed: %d", stageName, errorCode);
-
- unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portTestFlags);
+- (void) stageFailed:(const char*)stageName withError:(long)errorCode {
+ Log(LOG_I, @"Stage %s failed: %ld", stageName, errorCode);
dispatch_async(dispatch_get_main_queue(), ^{
- // Allow the display to go to sleep now
- [UIApplication sharedApplication].idleTimerDisabled = NO;
-
- NSString* message = [NSString stringWithFormat:@"%s failed with error %d", stageName, errorCode];
- if (portTestFlags != 0) {
- char failingPorts[256];
- LiStringifyPortFlags(portTestFlags, "\n", failingPorts, sizeof(failingPorts));
- message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
- }
- if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
- message = [message stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
- }
-
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
- message:message
+ message:[NSString stringWithFormat:@"%s failed with error %ld",
+ stageName, errorCode]
preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:alert];
- [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
+ [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:alert animated:YES completion:nil];
@@ -520,109 +122,22 @@
Log(LOG_I, @"Launch failed: %@", message);
dispatch_async(dispatch_get_main_queue(), ^{
- // Allow the display to go to sleep now
- [UIApplication sharedApplication].idleTimerDisabled = NO;
-
- UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Error"
+ UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
message:message
preferredStyle:UIAlertControllerStyleAlert];
- [Utils addHelpOptionToDialog:alert];
- [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
+ [alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:alert animated:YES completion:nil];
});
}
-- (void)rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor {
- Log(LOG_I, @"Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor);
-
- [_controllerSupport rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
+- (void) displayMessage:(const char*)message {
+ Log(LOG_I, @"Display message: %s", message);
}
-- (void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger {
- Log(LOG_I, @"Trigger rumble on gamepad %d: %04x %04x", controllerNumber, leftTrigger, rightTrigger);
-
- [_controllerSupport rumbleTriggers:controllerNumber leftTrigger:leftTrigger rightTrigger:rightTrigger];
-}
-
-- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz {
- Log(LOG_I, @"Set motion state on gamepad %d: %02x %u Hz", controllerNumber, motionType, reportRateHz);
-
- [_controllerSupport setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
-}
-
-- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
- Log(LOG_I, @"Set controller LED on gamepad %d: l%02x%02x%02x", controllerNumber, r, g, b);
-
- [_controllerSupport setControllerLed:controllerNumber r:r g:g b:b];
-}
-
-- (void)connectionStatusUpdate:(int)status {
- Log(LOG_W, @"Connection status update: %d", status);
-
- // The stats overlay takes precedence over these warnings
- if (_statsUpdateTimer != nil) {
- return;
- }
-
- dispatch_async(dispatch_get_main_queue(), ^{
- switch (status) {
- case CONN_STATUS_OKAY:
- [self updateOverlayText:nil];
- break;
-
- case CONN_STATUS_POOR:
- if (self->_streamConfig.bitRate > 5000) {
- [self updateOverlayText:@"Slow connection to PC\nReduce your bitrate"];
- }
- else {
- [self updateOverlayText:@"Poor connection to PC"];
- }
- break;
- }
- });
-}
-
-- (void) updatePreferredDisplayMode:(BOOL)streamActive {
-#if TARGET_OS_TV
- if (@available(tvOS 11.2, *)) {
- UIWindow* window = [[[UIApplication sharedApplication] delegate] window];
- AVDisplayManager* displayManager = [window avDisplayManager];
-
- // This logic comes from Kodi and MrMC
- if (streamActive) {
- int dynamicRange;
-
- if (LiGetCurrentHostDisplayHdrMode()) {
- dynamicRange = 2; // HDR10
- }
- else {
- dynamicRange = 0; // SDR
- }
-
- AVDisplayCriteria* displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:[_settings.framerate floatValue]
- videoDynamicRange:dynamicRange];
- displayManager.preferredDisplayCriteria = displayCriteria;
- }
- else {
- // Switch back to the default display mode
- displayManager.preferredDisplayCriteria = nil;
- }
- }
-#endif
-}
-
-- (void) setHdrMode:(bool)enabled {
- Log(LOG_I, @"HDR is now: %s", enabled ? "active" : "inactive");
- dispatch_async(dispatch_get_main_queue(), ^{
- [self updatePreferredDisplayMode:YES];
- });
-}
-
-- (void) videoContentShown {
- [_spinner stopAnimating];
- [self.view setBackgroundColor:[UIColor blackColor]];
+- (void) displayTransientMessage:(const char*)message {
+ Log(LOG_I, @"Display transient message: %s", message);
}
- (void)didReceiveMemoryWarning
@@ -631,84 +146,7 @@
// Dispose of any resources that can be recreated.
}
-- (void)gamepadPresenceChanged {
-#if !TARGET_OS_TV
- if (@available(iOS 11.0, *)) {
- [self setNeedsUpdateOfHomeIndicatorAutoHidden];
- }
-#endif
-}
-
-- (void)mousePresenceChanged {
-#if !TARGET_OS_TV
- if (@available(iOS 14.0, *)) {
- [self setNeedsUpdateOfPrefersPointerLocked];
- }
-#endif
-}
-
-- (void) streamExitRequested {
- Log(LOG_I, @"Gamepad combo requested stream exit");
-
- [self returnToMainFrame];
-}
-
-- (void)userInteractionBegan {
- // Disable hiding home bar when user is interacting.
- // iOS will force it to be shown anyway, but it will
- // also discard our edges deferring system gestures unless
- // we willingly give up home bar hiding preference.
- _userIsInteracting = YES;
-#if !TARGET_OS_TV
- if (@available(iOS 11.0, *)) {
- [self setNeedsUpdateOfHomeIndicatorAutoHidden];
- }
-#endif
-}
-
-- (void)userInteractionEnded {
- // Enable home bar hiding again if conditions allow
- _userIsInteracting = NO;
-#if !TARGET_OS_TV
- if (@available(iOS 11.0, *)) {
- [self setNeedsUpdateOfHomeIndicatorAutoHidden];
- }
-#endif
-}
-
-#if !TARGET_OS_TV
-// Require a confirmation when streaming to activate a system gesture
-- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
- return UIRectEdgeAll;
-}
-
-- (BOOL)prefersHomeIndicatorAutoHidden {
- if ([_controllerSupport getConnectedGamepadCount] > 0 &&
- [_streamView getCurrentOscState] == OnScreenControlsLevelOff &&
- _userIsInteracting == NO) {
- // Autohide the home bar when a gamepad is connected
- // and the on-screen controls are disabled. We can't
- // do this all the time because any touch on the display
- // will cause the home indicator to reappear, and our
- // preferredScreenEdgesDeferringSystemGestures will also
- // be suppressed (leading to possible errant exits of the
- // stream).
- return YES;
- }
-
- return NO;
-}
-
- (BOOL)shouldAutorotate {
return YES;
}
-
-- (BOOL)prefersPointerLocked {
- // Pointer lock breaks the UIKit mouse APIs, which is a problem because
- // GCMouse is horribly broken on iOS 14.0 for certain mice. Only lock
- // the cursor if there is a GCMouse present.
- return [GCMouse mice].count > 0;
-}
-#endif
-
@end
diff --git a/Limelight/main.m b/Limelight/main.m
index 0dbc0e3..b41a40e 100644
--- a/Limelight/main.m
+++ b/Limelight/main.m
@@ -10,13 +10,9 @@
#import "AppDelegate.h"
-#define SDL_MAIN_HANDLED
-#import
-
int main(int argc, char * argv[])
{
@autoreleasepool {
- SDL_SetMainReady();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
deleted file mode 100644
index 6707298..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "filename" : "bg-app.jpg"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/bg-app.jpg b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/bg-app.jpg
deleted file mode 100644
index 7d956c0..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/bg-app.jpg and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json
deleted file mode 100644
index d29f024..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "layers" : [
- {
- "filename" : "Front.imagestacklayer"
- },
- {
- "filename" : "Middle.imagestacklayer"
- },
- {
- "filename" : "Back.imagestacklayer"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
deleted file mode 100644
index 48ecb4f..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
deleted file mode 100644
index ece4178..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "filename" : "wedges-app.png"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/wedges-app.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/wedges-app.png
deleted file mode 100644
index 6704ca2..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/wedges-app.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
deleted file mode 100644
index f103f41..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "filename" : "bg.jpg",
- "scale" : "1x"
- },
- {
- "idiom" : "tv",
- "filename" : "bg@2.jpg",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bg.jpg b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bg.jpg
deleted file mode 100644
index 745d47e..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bg.jpg and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bg@2.jpg b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bg@2.jpg
deleted file mode 100644
index 6b4890e..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bg@2.jpg and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json
deleted file mode 100644
index d29f024..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "layers" : [
- {
- "filename" : "Front.imagestacklayer"
- },
- {
- "filename" : "Middle.imagestacklayer"
- },
- {
- "filename" : "Back.imagestacklayer"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
deleted file mode 100644
index 16a370d..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "scale" : "1x"
- },
- {
- "idiom" : "tv",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
deleted file mode 100644
index 2bddca3..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "filename" : "Wedges.png",
- "scale" : "1x"
- },
- {
- "idiom" : "tv",
- "filename" : "Wedges@2.png",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Wedges.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Wedges.png
deleted file mode 100644
index c45bdb3..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Wedges.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Wedges@2.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Wedges@2.png
deleted file mode 100644
index 925cc3a..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Wedges@2.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json
deleted file mode 100644
index db288f3..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "assets" : [
- {
- "size" : "1280x768",
- "idiom" : "tv",
- "filename" : "App Icon - App Store.imagestack",
- "role" : "primary-app-icon"
- },
- {
- "size" : "400x240",
- "idiom" : "tv",
- "filename" : "App Icon.imagestack",
- "role" : "primary-app-icon"
- },
- {
- "size" : "2320x720",
- "idiom" : "tv",
- "filename" : "Top Shelf Image Wide.imageset",
- "role" : "top-shelf-image-wide"
- },
- {
- "size" : "1920x720",
- "idiom" : "tv",
- "filename" : "Top Shelf Image.imageset",
- "role" : "top-shelf-image"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json
deleted file mode 100644
index 44273a5..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "filename" : "topshelf-wide@1.png",
- "scale" : "1x"
- },
- {
- "idiom" : "tv",
- "filename" : "topshelf-wide@2.png",
- "scale" : "2x"
- },
- {
- "idiom" : "tv-marketing",
- "filename" : "topshelf-wide@1-1.png",
- "scale" : "1x"
- },
- {
- "idiom" : "tv-marketing",
- "filename" : "topshelf-wide@2-1.png",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@1-1.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@1-1.png
deleted file mode 100644
index a14ef59..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@1-1.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@1.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@1.png
deleted file mode 100644
index a14ef59..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@1.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@2-1.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@2-1.png
deleted file mode 100644
index 5e532f4..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@2-1.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@2.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@2.png
deleted file mode 100644
index 5e532f4..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/topshelf-wide@2.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json
deleted file mode 100644
index bd8b0b6..0000000
--- a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "images" : [
- {
- "idiom" : "tv",
- "filename" : "topshelf@1.png",
- "scale" : "1x"
- },
- {
- "idiom" : "tv",
- "filename" : "topshelf@2.png",
- "scale" : "2x"
- },
- {
- "idiom" : "tv-marketing",
- "filename" : "topshelf@1-1.png",
- "scale" : "1x"
- },
- {
- "idiom" : "tv-marketing",
- "filename" : "topshelf@2-1.png",
- "scale" : "2x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@1-1.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@1-1.png
deleted file mode 100644
index 99646a7..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@1-1.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@1.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@1.png
deleted file mode 100644
index 99646a7..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@1.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2-1.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2-1.png
deleted file mode 100644
index 830a274..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2-1.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2.png b/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2.png
deleted file mode 100644
index 830a274..0000000
Binary files a/Moonlight TV/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/topshelf@2.png and /dev/null differ
diff --git a/Moonlight TV/Assets.xcassets/Contents.json b/Moonlight TV/Assets.xcassets/Contents.json
deleted file mode 100644
index da4a164..0000000
--- a/Moonlight TV/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/Launch Image.launchimage/Contents.json b/Moonlight TV/Assets.xcassets/Launch Image.launchimage/Contents.json
deleted file mode 100644
index c446e92..0000000
--- a/Moonlight TV/Assets.xcassets/Launch Image.launchimage/Contents.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "images" : [
- {
- "orientation" : "landscape",
- "idiom" : "tv",
- "filename" : "Simulator Screen Shot - Apple TV 4K - 2019-09-22 at 20.39.12.png",
- "extent" : "full-screen",
- "minimum-system-version" : "11.0",
- "scale" : "2x"
- },
- {
- "orientation" : "landscape",
- "idiom" : "tv",
- "extent" : "full-screen",
- "minimum-system-version" : "9.0",
- "scale" : "1x"
- }
- ],
- "info" : {
- "version" : 1,
- "author" : "xcode"
- }
-}
\ No newline at end of file
diff --git a/Moonlight TV/Assets.xcassets/Launch Image.launchimage/Simulator Screen Shot - Apple TV 4K - 2019-09-22 at 20.39.12.png b/Moonlight TV/Assets.xcassets/Launch Image.launchimage/Simulator Screen Shot - Apple TV 4K - 2019-09-22 at 20.39.12.png
deleted file mode 100644
index c518271..0000000
Binary files a/Moonlight TV/Assets.xcassets/Launch Image.launchimage/Simulator Screen Shot - Apple TV 4K - 2019-09-22 at 20.39.12.png and /dev/null differ
diff --git a/Moonlight TV/Base.lproj/Main.storyboard b/Moonlight TV/Base.lproj/Main.storyboard
deleted file mode 100644
index 7c0413f..0000000
--- a/Moonlight TV/Base.lproj/Main.storyboard
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Moonlight TV/Info.plist b/Moonlight TV/Info.plist
deleted file mode 100644
index c1f0bee..0000000
--- a/Moonlight TV/Info.plist
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleDisplayName
- Moonlight
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(MARKETING_VERSION)
- CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
- GCSupportedGameControllers
-
-
- ProfileName
- ExtendedGamepad
-
-
- GCSupportsControllerUserInteraction
-
- ITSAppUsesNonExemptEncryption
-
- LSRequiresIPhoneOS
-
- NSAppTransportSecurity
-
- NSAllowsArbitraryLoads
-
-
- NSBluetoothAlwaysUsageDescription
- Bluetooth access allows Moonlight to connect to Citrix X1 mice.
- NSBluetoothPeripheralUsageDescription
- Bluetooth access allows Moonlight to connect to Citrix X1 mice.
- NSBonjourServices
-
- _nvstream._tcp
-
- NSLocalNetworkUsageDescription
- Moonlight uses the local network to connect to your gaming PC for streaming.
- UIMainStoryboardFile
- Main
- UIRequiredDeviceCapabilities
-
- arm64
-
- LSSupportsGameMode
-
- UIUserInterfaceStyle
- Dark
-
-
diff --git a/Moonlight TV/Settings.bundle/Root.plist b/Moonlight TV/Settings.bundle/Root.plist
deleted file mode 100644
index 8088188..0000000
--- a/Moonlight TV/Settings.bundle/Root.plist
+++ /dev/null
@@ -1,259 +0,0 @@
-
-
-
-
- StringsTable
- Root
- PreferenceSpecifiers
-
-
- Type
- PSGroupSpecifier
- Title
- Basic Configuration
-
-
- Type
- PSMultiValueSpecifier
- Title
- Stream Resolution
- Key
- streamResolution
- DefaultValue
- 1
- Values
-
- 0
- 1
- 3
- 2
-
- Titles
-
- 720p
- 1080p
- 1440p
- 4K
-
-
-
- Type
- PSMultiValueSpecifier
- Title
- Frame Rate
- Key
- framerate
- DefaultValue
- 60
- Values
-
- 30
- 60
-
- Titles
-
- 30 FPS
- 60 FPS
-
-
-
- Type
- PSMultiValueSpecifier
- Title
- Bitrate
- Key
- bitrate
- DefaultValue
- 20000
- Titles
-
- 0.5 Mbps
- 1 Mbps
- 2 Mbps
- 5 Mbps
- 10 Mbps
- 20 Mbps
- 25 Mbps
- 50 Mbps
- 70 Mbps
- 80 Mbps
- 100 Mbps
- 150 Mbps
-
- Values
-
- 500
- 1000
- 2000
- 5000
- 10000
- 20000
- 25000
- 50000
- 70000
- 80000
- 100000
- 150000
-
-
-
- Type
- PSMultiValueSpecifier
- Title
- Audio Configuration
- Key
- audioConfig
- DefaultValue
- 2
- Titles
-
- Stereo
- LPCM 5.1
- LPCM 7.1
-
- Values
-
- 2
- 6
- 8
-
-
-
- Type
- PSGroupSpecifier
- Title
- Host Options
-
-
- Type
- PSToggleSwitchSpecifier
- DefaultValue
-
- Title
- Play Audio on Host PC
- Key
- audioOnPC
-
-
- Type
- PSToggleSwitchSpecifier
- Title
- Optimize Game Settings
- Key
- optimizeGames
- DefaultValue
-
-
-
- Type
- PSGroupSpecifier
- Title
- Input Options
-
-
- Type
- PSToggleSwitchSpecifier
- Title
- Automatic Game Controller Detection
- Key
- multipleControllers
- DefaultValue
-
-
-
- Type
- PSToggleSwitchSpecifier
- Title
- Swap A/B and X/Y Buttons
- Key
- swapABXYButtons
- DefaultValue
-
-
-
- Type
- PSToggleSwitchSpecifier
- Title
- Citrix X1 Mouse Support
- Key
- btMouseSupport
- DefaultValue
-
-
-
- Type
- PSGroupSpecifier
- Title
- Video Options
-
-
- Type
- PSMultiValueSpecifier
- Title
- Preferred Codec
- Key
- preferredCodec
- DefaultValue
- 0
- Values
-
- 0
- 1
- 2
-
- Titles
-
- Auto
- H.264
- HEVC
-
-
-
- Type
- PSMultiValueSpecifier
- Title
- Frame Pacing Preference
- Key
- useFramePacing
- DefaultValue
- 0
- Values
-
- 0
- 1
-
- Titles
-
- Lowest Latency
- Smoothest Video
-
-
-
- Type
- PSToggleSwitchSpecifier
- Title
- Request HDR Video
- Key
- enableHdr
- DefaultValue
-
-
-
- Type
- PSGroupSpecifier
- Title
- Troubleshooting
-
-
- Type
- PSToggleSwitchSpecifier
- Title
- Statistics Overlay
- Key
- statsOverlay
- DefaultValue
-
-
-
-
-
diff --git a/Moonlight TV/Settings.bundle/en.lproj/Root.strings b/Moonlight TV/Settings.bundle/en.lproj/Root.strings
deleted file mode 100644
index 8cd87b9..0000000
Binary files a/Moonlight TV/Settings.bundle/en.lproj/Root.strings and /dev/null differ
diff --git a/Moonlight TV/main.m b/Moonlight TV/main.m
deleted file mode 100644
index df13448..0000000
--- a/Moonlight TV/main.m
+++ /dev/null
@@ -1,20 +0,0 @@
-//
-// main.m
-// Moonlight TV
-//
-// Created by Diego Waxemberg on 8/25/18.
-// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
-//
-
-#import
-#import "AppDelegate.h"
-
-#define SDL_MAIN_HANDLED
-#import
-
-int main(int argc, char * argv[]) {
- @autoreleasepool {
- SDL_SetMainReady();
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- }
-}
diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj
index e686ec1..875ed3b 100644
--- a/Moonlight.xcodeproj/project.pbxproj
+++ b/Moonlight.xcodeproj/project.pbxproj
@@ -3,90 +3,23 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 54;
+ objectVersion = 47;
objects = {
/* Begin PBXBuildFile section */
- 693B3A9B218638CD00982F7B /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 693B3A9A218638CD00982F7B /* Settings.bundle */; };
- 98181BE92791277400E43572 /* libSDL2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98181BE82791275D00E43572 /* libSDL2.a */; };
- 98181BEB2791278F00E43572 /* libSDL2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98181BEA2791278300E43572 /* libSDL2.a */; };
- 98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98181BEC2791281100E43572 /* CoreMotion.framework */; };
- 9819CC14254F107A008A7C8E /* RelativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */; };
- 9819CC1D254F1730008A7C8E /* RelativeTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */; };
- 9819CC22254F180F008A7C8E /* AbsoluteTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */; };
- 9827E7A32514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; };
- 9827E7A42514366900F25707 /* HapticContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 9827E7A22514366900F25707 /* HapticContext.m */; };
9832D1361BBCD5C50036EF48 /* TemporaryApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 9832D1351BBCD5C50036EF48 /* TemporaryApp.m */; };
- 9865DC30213260B40005B9B9 /* libmoonlight-common-tv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A68152132509400507771 /* libmoonlight-common-tv.a */; };
- 9865DC31213260F10005B9B9 /* mkcert.c in Sources */ = {isa = PBXBuildFile; fileRef = FB89460719F646E200339C8A /* mkcert.c */; };
- 9865DC36213287F30005B9B9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = FB290D0319B2C406004C83CF /* AppDelegate.m */; };
- 9865DC37213287FE0005B9B9 /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; };
- 9865DC38213287FE0005B9B9 /* LoadingFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */; };
- 9865DC3C2132922E0005B9B9 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9865DC3B2132922E0005B9B9 /* GameController.framework */; };
- 9865DC3E21332D660005B9B9 /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
- 98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A032AF60F5300C5A11C /* libavcodec.a */; };
- 98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A052AF60F5300C5A11C /* libavformat.a */; };
- 98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A042AF60F5300C5A11C /* libavutil.a */; };
- 98882A0C2AF60F9E00C5A11C /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A0A2AF60F9000C5A11C /* libavcodec.a */; };
- 98882A0D2AF60FA000C5A11C /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A0B2AF60F9000C5A11C /* libavformat.a */; };
- 98882A0E2AF60FA300C5A11C /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A092AF60F9000C5A11C /* libavutil.a */; };
- 988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = 988FCD40293B091B003050E2 /* KeyboardInputField.m */; };
- 988FCD42293B091B003050E2 /* KeyboardInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = 988FCD40293B091B003050E2 /* KeyboardInputField.m */; };
- 9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9890CF6A203B7EE1006C4B06 /* libxml2.tbd */; };
- 9896219723D15C7000211983 /* X1Kit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9896219623D15C7000211983 /* X1Kit.swift */; };
- 9896219A23D56E8100211983 /* X1Kit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9896219623D15C7000211983 /* X1Kit.swift */; };
- 9897B6A1221260EF00966419 /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = 9897B6A0221260EF00966419 /* Controller.m */; };
- 9897B6A62212732C00966419 /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = 9897B6A0221260EF00966419 /* Controller.m */; };
- 98B9CE6D27B2144B00B473C4 /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98B9CE6C27B2144B00B473C4 /* AVKit.framework */; };
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; };
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; };
- DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
- F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
- F74BEFA42C1A80E400224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEFA32C1A80E400224667 /* OpenSSL */; };
- FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
- FB1A67602132419700507771 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB1A675E2132419700507771 /* Main.storyboard */; };
- FB1A67622132419A00507771 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB1A67612132419A00507771 /* Assets.xcassets */; };
- FB1A67A12132458C00507771 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A67642132419A00507771 /* main.m */; };
- FB1A67A3213245BD00507771 /* Connection.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461719F646E200339C8A /* Connection.m */; };
- FB1A67A5213245BD00507771 /* StreamConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461919F646E200339C8A /* StreamConfiguration.m */; };
- FB1A67A7213245BD00507771 /* StreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461B19F646E200339C8A /* StreamManager.m */; };
- FB1A67A9213245BD00507771 /* VideoDecoderRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */; };
- FB1A67AB213245C500507771 /* CryptoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89460619F646E200339C8A /* CryptoManager.m */; };
- FB1A67AF213245C500507771 /* IdManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB53E1421BE5DC4400CD6ECE /* IdManager.m */; };
- FB1A67B1213245D500507771 /* HttpManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461019F646E200339C8A /* HttpManager.m */; };
- FB1A67B3213245D500507771 /* HttpRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD361A7E02DB00872C98 /* HttpRequest.m */; };
- FB1A67B5213245D500507771 /* HttpResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD271A7C84ED00872C98 /* HttpResponse.m */; };
- FB1A67B7213245D500507771 /* ServerInfoResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD391A7E05CE00872C98 /* ServerInfoResponse.m */; };
- FB1A67B9213245D500507771 /* AppAssetResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD3C1A7E111600872C98 /* AppAssetResponse.m */; };
- FB1A67BB213245D500507771 /* AppListResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD3F1A7E127D00872C98 /* AppListResponse.m */; };
- FB1A67BD213245E000507771 /* DiscoveryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4678F91A55FFAD00377732 /* DiscoveryManager.m */; };
- FB1A67BF213245E000507771 /* DiscoveryWorker.m in Sources */ = {isa = PBXBuildFile; fileRef = FB6549551A57907E001C8F39 /* DiscoveryWorker.m */; };
- FB1A67C1213245E000507771 /* MDNSManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461219F646E200339C8A /* MDNSManager.m */; };
- FB1A67C3213245EA00507771 /* AppAssetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494219FC9C04002D2A60 /* AppAssetManager.m */; };
- FB1A67C5213245EA00507771 /* AppAssetRetriever.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD311A7D867C00872C98 /* AppAssetRetriever.m */; };
- FB1A67C7213245EA00507771 /* PairManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461419F646E200339C8A /* PairManager.m */; };
- FB1A67C9213245EA00507771 /* WakeOnLanManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4678FE1A565DAC00377732 /* WakeOnLanManager.m */; };
- FB1A67CB213245EA00507771 /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
- FB1A67CD213245F800507771 /* DataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD349611A0089F6002D2A60 /* DataManager.m */; };
- FB1A67CF213245F800507771 /* TemporaryApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 9832D1351BBCD5C50036EF48 /* TemporaryApp.m */; };
- FB1A67D1213245F800507771 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
- FB1A67D3213245F800507771 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; };
- FB1A67D52132460400507771 /* ControllerSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89460B19F646E200339C8A /* ControllerSupport.m */; };
- FB1A67D82132460400507771 /* StreamView.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89460D19F646E200339C8A /* StreamView.m */; };
- FB1A67DA2132460400507771 /* OnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4678EC1A50C40900377732 /* OnScreenControls.m */; };
- FB1A67DC2132460400507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
- FB1A67DE2132460A00507771 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462219F646E200339C8A /* Utils.m */; };
- FB1A67E02132460A00507771 /* Logger.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD1C8E11A8AD71400C6703C /* Logger.m */; };
- FB1A67E32132498A00507771 /* Limelight.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */; };
- FB1A67E521324A1F00507771 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A67E421324A1F00507771 /* CoreData.framework */; };
- FB1A67E821324DE300507771 /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946EA19F6AFB800339C8A /* libopus.a */; };
- FB1A67EA21324DF300507771 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A67E921324DF300507771 /* libxml2.tbd */; };
- FB1A6819213284FB00507771 /* UIComputerView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86DF19F7A837001C18A8 /* UIComputerView.m */; };
- FB1A681A213284FB00507771 /* UIAppView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E519F82297001C18A8 /* UIAppView.m */; };
- FB1A681B213284FB00507771 /* ComputerScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1D59961BBCCB6400F482CA /* ComputerScrollView.m */; };
- FB1A681C213284FB00507771 /* AppCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */; };
- FB1A681E21328A1B00507771 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB290D0819B2C406004C83CF /* Images.xcassets */; };
+ 9E5D600B1A5A5A3900689918 /* Apache License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D5FF81A5A5A3900689918 /* Apache License.txt */; };
+ 9E5D600C1A5A5A3900689918 /* Roboto-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D5FF91A5A5A3900689918 /* Roboto-Black.ttf */; };
+ 9E5D600E1A5A5A3900689918 /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D5FFB1A5A5A3900689918 /* Roboto-Bold.ttf */; };
+ 9E5D60111A5A5A3900689918 /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D5FFE1A5A5A3900689918 /* Roboto-Light.ttf */; };
+ 9E5D60131A5A5A3900689918 /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D60001A5A5A3900689918 /* Roboto-Medium.ttf */; };
+ 9E5D60151A5A5A3900689918 /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D60021A5A5A3900689918 /* Roboto-Regular.ttf */; };
+ 9E5D60161A5A5A3900689918 /* Roboto-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 9E5D60031A5A5A3900689918 /* Roboto-Thin.ttf */; };
+ D46A73AD1CBC7D090039F1EE /* ControllerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46A73AC1CBC7D090039F1EE /* ControllerUnitTests.swift */; };
+ D4746EEC1CBC740C006FB401 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4746EEB1CBC740C006FB401 /* Controller.swift */; };
FB1D59971BBCCB6400F482CA /* ComputerScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1D59961BBCCB6400F482CA /* ComputerScrollView.m */; };
FB1D599A1BBCCD7E00F482CA /* AppCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1D59991BBCCD7E00F482CA /* AppCollectionView.m */; };
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF119B2C406004C83CF /* Foundation.framework */; };
@@ -98,6 +31,9 @@
FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = FB290D0319B2C406004C83CF /* AppDelegate.m */; };
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */; };
FB290D0919B2C406004C83CF /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB290D0819B2C406004C83CF /* Images.xcassets */; };
+ FB290DB719B2C870004C83CF /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290DB619B2C870004C83CF /* libz.dylib */; };
+ FB290DB919B2C877004C83CF /* libbz2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290DB819B2C877004C83CF /* libbz2.dylib */; };
+ FB290DC419B2E98F004C83CF /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290DC319B2E98F004C83CF /* libxml2.dylib */; };
FB290E7919B37D81004C83CF /* iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB290E7819B37D81004C83CF /* iPad.storyboard */; };
FB290E7B19B38036004C83CF /* iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB290E7A19B38036004C83CF /* iPhone.storyboard */; };
FB4678ED1A50C40900377732 /* OnScreenControls.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4678EC1A50C40900377732 /* OnScreenControls.m */; };
@@ -107,6 +43,7 @@
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */; };
FB53E1431BE5DC4400CD6ECE /* IdManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB53E1421BE5DC4400CD6ECE /* IdManager.m */; };
FB6549561A57907E001C8F39 /* DiscoveryWorker.m in Sources */ = {isa = PBXBuildFile; fileRef = FB6549551A57907E001C8F39 /* DiscoveryWorker.m */; };
+ FB7E794419C8B71B00A15F68 /* libiconv.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = FB7E794319C8B71B00A15F68 /* libiconv.dylib */; };
FB89462819F646E200339C8A /* CryptoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89460619F646E200339C8A /* CryptoManager.m */; };
FB89462919F646E200339C8A /* mkcert.c in Sources */ = {isa = PBXBuildFile; fileRef = FB89460719F646E200339C8A /* mkcert.c */; };
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89460B19F646E200339C8A /* ControllerSupport.m */; };
@@ -122,6 +59,8 @@
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; };
FB89463819F6473800339C8A /* Launch Screen.xib in Resources */ = {isa = PBXBuildFile; fileRef = FB89463719F6473800339C8A /* Launch Screen.xib */; };
+ FB8946EB19F6AFE100339C8A /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E019F6AFB800339C8A /* libcrypto.a */; };
+ FB8946EC19F6AFE400339C8A /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E119F6AFB800339C8A /* libssl.a */; };
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946EA19F6AFB800339C8A /* libopus.a */; };
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD271A7C84ED00872C98 /* HttpResponse.m */; };
FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD311A7D867C00872C98 /* AppAssetRetriever.m */; };
@@ -133,9 +72,12 @@
FBD3494319FC9C04002D2A60 /* AppAssetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494219FC9C04002D2A60 /* AppAssetManager.m */; };
FBD3495019FF2174002D2A60 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494F19FF2174002D2A60 /* SettingsViewController.m */; };
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495219FF36FB002D2A60 /* SWRevealViewController.m */; };
+ FBD3495E1A004412002D2A60 /* Settings.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3495D1A004412002D2A60 /* Settings.m */; };
FBD349621A0089F6002D2A60 /* DataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD349611A0089F6002D2A60 /* DataManager.m */; };
FBDE86E019F7A837001C18A8 /* UIComputerView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86DF19F7A837001C18A8 /* UIComputerView.m */; };
FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E519F82297001C18A8 /* UIAppView.m */; };
+ FBFCB3311B50B29400089F8A /* Host.m in Sources */ = {isa = PBXBuildFile; fileRef = FBFCB3301B50B29400089F8A /* Host.m */; };
+ FBFCB3351B50B29400089F8A /* App.m in Sources */ = {isa = PBXBuildFile; fileRef = FBFCB3341B50B29400089F8A /* App.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -153,77 +95,36 @@
remoteGlobalIDString = FB290E2D19B37A4E004C83CF;
remoteInfo = "moonlight-common";
};
- FB1A68142132509400507771 /* PBXContainerItemProxy */ = {
+ D46A73A51CBC7CB60039F1EE /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
- containerPortal = 98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */;
- proxyType = 2;
- remoteGlobalIDString = FB1A67EF21324EE300507771;
- remoteInfo = "moonlight-common-tv";
- };
- FB1A68162132509800507771 /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */;
+ containerPortal = FB290CE619B2C406004C83CF /* Project object */;
proxyType = 1;
- remoteGlobalIDString = FB1A67EE21324EE300507771;
- remoteInfo = "moonlight-common-tv";
+ remoteGlobalIDString = FB290CED19B2C406004C83CF;
+ remoteInfo = Moonlight;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
- 566E9D2B2770B23A00EF7BFE /* Moonlight v1.7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.7.xcdatamodel"; sourceTree = ""; };
- 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; };
- 9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConnectionCallbacks.h; sourceTree = ""; };
- 98132E8C20BC9A62007A053F /* Moonlight v1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.1.xcdatamodel"; sourceTree = ""; };
- 98181BE82791275D00E43572 /* libSDL2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libSDL2.a; path = libs/SDL2/lib/iOS/libSDL2.a; sourceTree = ""; };
- 98181BEA2791278300E43572 /* libSDL2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libSDL2.a; path = libs/SDL2/lib/tvOS/libSDL2.a; sourceTree = ""; };
- 98181BEC2791281100E43572 /* CoreMotion.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMotion.framework; path = System/Library/Frameworks/CoreMotion.framework; sourceTree = SDKROOT; };
- 9819CC12254F107A008A7C8E /* RelativeTouchHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RelativeTouchHandler.h; sourceTree = ""; };
- 9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RelativeTouchHandler.m; sourceTree = ""; };
- 9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AbsoluteTouchHandler.h; sourceTree = ""; };
- 9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsoluteTouchHandler.m; sourceTree = ""; };
- 9819CC25254F2734008A7C8E /* Moonlight v1.6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.6.xcdatamodel"; sourceTree = ""; };
- 9827E7A22514366900F25707 /* HapticContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HapticContext.m; sourceTree = ""; };
- 9827E7A7251436EA00F25707 /* HapticContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HapticContext.h; sourceTree = ""; };
9832D1341BBCD5C50036EF48 /* TemporaryApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryApp.h; path = Database/TemporaryApp.h; sourceTree = ""; };
9832D1351BBCD5C50036EF48 /* TemporaryApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryApp.m; path = Database/TemporaryApp.m; sourceTree = ""; };
- 984CD023288A310D0097D2D4 /* Moonlight v1.8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.8.xcdatamodel"; sourceTree = ""; };
- 98517B1B21CE0A9000481377 /* Moonlight v1.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.3.xcdatamodel"; sourceTree = ""; };
- 98608BDD22DC0C2C000E5672 /* Moonlight v1.4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.4.xcdatamodel"; sourceTree = ""; };
- 9865DC3B2132922E0005B9B9 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.4.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
- 986CCE6C2133E45300168291 /* Moonlight v1.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.2.xcdatamodel"; sourceTree = ""; };
- 986E28A528EA989100758361 /* Moonlight v1.9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.9.xcdatamodel"; sourceTree = ""; };
- 9874E8962AE95E1D00130A3C /* Moonlight v1.10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.10.xcdatamodel"; sourceTree = ""; };
- 98783FEA242EAC5D00F00EF4 /* Moonlight v1.5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.5.xcdatamodel"; sourceTree = ""; };
- 98882A032AF60F5300C5A11C /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = libs/FFmpeg/lib/iOS/libavcodec.a; sourceTree = ""; };
- 98882A042AF60F5300C5A11C /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = libs/FFmpeg/lib/iOS/libavutil.a; sourceTree = ""; };
- 98882A052AF60F5300C5A11C /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = libs/FFmpeg/lib/iOS/libavformat.a; sourceTree = ""; };
- 98882A092AF60F9000C5A11C /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = libs/FFmpeg/lib/tvOS/libavutil.a; sourceTree = ""; };
- 98882A0A2AF60F9000C5A11C /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = libs/FFmpeg/lib/tvOS/libavcodec.a; sourceTree = ""; };
- 98882A0B2AF60F9000C5A11C /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = libs/FFmpeg/lib/tvOS/libavformat.a; sourceTree = ""; };
- 988FCD3F293B091B003050E2 /* KeyboardInputField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardInputField.h; sourceTree = ""; };
- 988FCD40293B091B003050E2 /* KeyboardInputField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardInputField.m; sourceTree = ""; };
- 9890CF6A203B7EE1006C4B06 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
- 9896219623D15C7000211983 /* X1Kit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = X1Kit.swift; path = X1Kit/Sources/X1Kit/X1Kit.swift; sourceTree = ""; };
- 9897B6A0221260EF00966419 /* Controller.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Controller.m; sourceTree = ""; };
- 9897B6A32212610800966419 /* Controller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Controller.h; sourceTree = ""; };
98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "moonlight-common.xcodeproj"; path = "moonlight-common/moonlight-common.xcodeproj"; sourceTree = ""; };
- 98B9CE6C27B2144B00B473C4 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.2.sdk/System/Library/Frameworks/AVKit.framework; sourceTree = DEVELOPER_DIR; };
98D5856B1C0EA79600F6CC00 /* TemporaryHost.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryHost.h; path = Database/TemporaryHost.h; sourceTree = ""; };
98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryHost.m; path = Database/TemporaryHost.m; sourceTree = ""; };
98D5856E1C0ED0E800F6CC00 /* TemporarySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporarySettings.h; path = Database/TemporarySettings.h; sourceTree = ""; };
98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporarySettings.m; path = Database/TemporarySettings.m; sourceTree = ""; };
+ 9E5D5FF81A5A5A3900689918 /* Apache License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "Apache License.txt"; sourceTree = ""; };
+ 9E5D5FF91A5A5A3900689918 /* Roboto-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Black.ttf"; sourceTree = ""; };
+ 9E5D5FFB1A5A5A3900689918 /* Roboto-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Bold.ttf"; sourceTree = ""; };
+ 9E5D5FFE1A5A5A3900689918 /* Roboto-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Light.ttf"; sourceTree = ""; };
+ 9E5D60001A5A5A3900689918 /* Roboto-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Medium.ttf"; sourceTree = ""; };
+ 9E5D60021A5A5A3900689918 /* Roboto-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Regular.ttf"; sourceTree = ""; };
+ 9E5D60031A5A5A3900689918 /* Roboto-Thin.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Thin.ttf"; sourceTree = ""; };
+ D46A73A01CBC7CB60039F1EE /* MoonlightUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MoonlightUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ D46A73A41CBC7CB60039F1EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ D46A73AB1CBC7D080039F1EE /* MoonlightUnitTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MoonlightUnitTests-Bridging-Header.h"; sourceTree = ""; };
+ D46A73AC1CBC7D090039F1EE /* ControllerUnitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerUnitTests.swift; path = Input/ControllerUnitTests.swift; sourceTree = ""; };
D4746EEA1CBC740C006FB401 /* Moonlight-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Moonlight-Bridging-Header.h"; path = "Input/Moonlight-Bridging-Header.h"; sourceTree = ""; };
- DC1F5A05206436B10037755F /* ConnectionHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConnectionHelper.h; sourceTree = ""; };
- DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = ""; };
- FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = ""; };
- FB1A674C2131E65900507771 /* KeyboardSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardSupport.m; sourceTree = ""; };
- FB1A67532132419700507771 /* Moonlight TV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Moonlight TV.app"; sourceTree = BUILT_PRODUCTS_DIR; };
- FB1A675F2132419700507771 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
- FB1A67612132419A00507771 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- FB1A67632132419A00507771 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- FB1A67642132419A00507771 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
- FB1A67E421324A1F00507771 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.4.sdk/System/Library/Frameworks/CoreData.framework; sourceTree = DEVELOPER_DIR; };
- FB1A67E921324DF300507771 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.4.sdk/usr/lib/libxml2.tbd; sourceTree = DEVELOPER_DIR; };
+ D4746EEB1CBC740C006FB401 /* Controller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = ""; };
FB1D59951BBCCB6400F482CA /* ComputerScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ComputerScrollView.h; sourceTree = ""; };
FB1D59961BBCCB6400F482CA /* ComputerScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ComputerScrollView.m; sourceTree = ""; };
FB1D59981BBCCD7E00F482CA /* AppCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppCollectionView.h; sourceTree = ""; };
@@ -241,6 +142,9 @@
FB290D0319B2C406004C83CF /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
FB290D0619B2C406004C83CF /* Limelight.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Limelight.xcdatamodel; sourceTree = ""; };
FB290D0819B2C406004C83CF /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; };
+ FB290DB619B2C870004C83CF /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
+ FB290DB819B2C877004C83CF /* libbz2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libbz2.dylib; path = usr/lib/libbz2.dylib; sourceTree = SDKROOT; };
+ FB290DC319B2E98F004C83CF /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; };
FB290E7819B37D81004C83CF /* iPad.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = iPad.storyboard; sourceTree = SOURCE_ROOT; };
FB290E7A19B38036004C83CF /* iPhone.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = iPhone.storyboard; sourceTree = SOURCE_ROOT; };
FB4678EB1A50C40900377732 /* OnScreenControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnScreenControls.h; sourceTree = ""; };
@@ -259,6 +163,8 @@
FB6549541A57907E001C8F39 /* DiscoveryWorker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DiscoveryWorker.h; sourceTree = ""; };
FB6549551A57907E001C8F39 /* DiscoveryWorker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DiscoveryWorker.m; sourceTree = ""; };
FB6549621A60B4A9001C8F39 /* Limelight 0.3.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Limelight 0.3.1.xcdatamodel"; sourceTree = ""; };
+ FB6DDEF31AE739100096499A /* libmoonlight-common.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libmoonlight-common.a"; path = "limelight-common-c/build/Debug-iphoneos/libmoonlight-common.a"; sourceTree = ""; };
+ FB7E794319C8B71B00A15F68 /* libiconv.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libiconv.dylib; path = usr/lib/libiconv.dylib; sourceTree = SDKROOT; };
FB89460519F646E200339C8A /* CryptoManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CryptoManager.h; sourceTree = ""; };
FB89460619F646E200339C8A /* CryptoManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CryptoManager.m; sourceTree = ""; };
FB89460719F646E200339C8A /* mkcert.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mkcert.c; sourceTree = ""; };
@@ -288,6 +194,83 @@
FB89462619F646E200339C8A /* StreamFrameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamFrameViewController.h; sourceTree = ""; };
FB89462719F646E200339C8A /* StreamFrameViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamFrameViewController.m; sourceTree = ""; };
FB89463719F6473800339C8A /* Launch Screen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = "Launch Screen.xib"; sourceTree = SOURCE_ROOT; };
+ FB89469419F6AFB800339C8A /* aes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = ""; };
+ FB89469519F6AFB800339C8A /* asn1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1.h; sourceTree = ""; };
+ FB89469619F6AFB800339C8A /* asn1_mac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1_mac.h; sourceTree = ""; };
+ FB89469719F6AFB800339C8A /* asn1t.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1t.h; sourceTree = ""; };
+ FB89469819F6AFB800339C8A /* bio.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bio.h; sourceTree = ""; };
+ FB89469919F6AFB800339C8A /* blowfish.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = blowfish.h; sourceTree = ""; };
+ FB89469A19F6AFB800339C8A /* bn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bn.h; sourceTree = ""; };
+ FB89469B19F6AFB800339C8A /* buffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = buffer.h; sourceTree = ""; };
+ FB89469C19F6AFB800339C8A /* camellia.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = camellia.h; sourceTree = ""; };
+ FB89469D19F6AFB800339C8A /* cast.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cast.h; sourceTree = ""; };
+ FB89469E19F6AFB800339C8A /* cmac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cmac.h; sourceTree = ""; };
+ FB89469F19F6AFB800339C8A /* cms.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cms.h; sourceTree = ""; };
+ FB8946A019F6AFB800339C8A /* comp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = comp.h; sourceTree = ""; };
+ FB8946A119F6AFB800339C8A /* conf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = conf.h; sourceTree = ""; };
+ FB8946A219F6AFB800339C8A /* conf_api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = conf_api.h; sourceTree = ""; };
+ FB8946A319F6AFB800339C8A /* crypto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = crypto.h; sourceTree = "