Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6273f2bb29 | ||
|
|
fac48ecf42 | ||
|
|
b5bf26bb2d | ||
|
|
00fab87b4c | ||
|
|
3554b40962 | ||
|
|
aa3752a905 | ||
|
|
c0a3ae9371 | ||
|
|
47850cd658 | ||
|
|
1b98870b41 | ||
|
|
ef98976ebf | ||
|
|
ba022da665 | ||
|
|
ae9abf2911 | ||
|
|
99e7f59df4 | ||
|
|
8273173080 | ||
|
|
3357b3882b | ||
|
|
b7510717c8 | ||
|
|
5434542877 | ||
|
|
4c2f6b269a | ||
|
|
0ccf046c69 | ||
|
|
f0b6602b99 | ||
|
|
72e0da4319 | ||
|
|
d2ad8b12ce | ||
|
|
6ef492662b | ||
|
|
3c7f33a80c | ||
|
|
eeec1f1cf5 | ||
|
|
45545e8f9b | ||
|
|
85b65d7112 | ||
|
|
26f7a27117 | ||
|
|
ea78cbc47e | ||
|
|
ea24b39fbe | ||
|
|
01a2853032 | ||
|
|
bfca061694 | ||
|
|
4a9bb00e0d | ||
|
|
72271a0c74 | ||
|
|
1ca787822b | ||
|
|
39186a4edf | ||
|
|
528b44f2f2 | ||
|
|
dfe2addca2 | ||
|
|
44bbba805f | ||
|
|
f756451001 | ||
|
|
b2236b5dad | ||
|
|
8dd62a8645 | ||
|
|
29f676a6d1 | ||
|
|
d83ac48a73 | ||
|
|
bf6a2e571f | ||
|
|
1f19d4de9d | ||
|
|
f4f3d7df50 | ||
|
|
a3f7aa41a1 | ||
|
|
101096ad80 | ||
|
|
34d871403c | ||
|
|
05e969221a | ||
|
|
c36fb14b15 | ||
|
|
c5dc7001f8 | ||
|
|
cd709000d7 | ||
|
|
a359aee36c | ||
|
|
185a4334ca | ||
|
|
6b10497e3e | ||
|
|
97f968fb0d | ||
|
|
127c53e341 | ||
|
|
3178f87707 | ||
|
|
9e8903402b | ||
|
|
af43539015 | ||
|
|
ca2fe9b0a3 | ||
|
|
84316166eb | ||
|
|
613f82b0ac | ||
|
|
f2ae32c45c | ||
|
|
4c646e36c7 | ||
|
|
85ee4f6518 | ||
|
|
37428c8c77 | ||
|
|
283a386b9c | ||
|
|
4d7715565a | ||
|
|
5ffdc1d2bd | ||
|
|
35e2ca1e00 | ||
|
|
b1938610dd | ||
|
|
f14a2a66dd | ||
|
|
44a7494823 | ||
|
|
f909371a90 | ||
|
|
f3b3d2bd08 | ||
|
|
e56923a71d | ||
|
|
f133f293b8 | ||
|
|
42773241c1 | ||
|
|
b8d256b73d | ||
|
|
0cb4c31d1e | ||
|
|
02fbd5f1d2 | ||
|
|
70f3a91dfb | ||
|
|
93063f5606 | ||
|
|
13f39e30f6 | ||
|
|
8ae0e4a547 | ||
|
|
3a6472eb0b | ||
|
|
037df87585 | ||
|
|
9b6865a53b | ||
|
|
4c3bc16593 | ||
|
|
440631fe2f | ||
|
|
374caee973 | ||
|
|
6225af1e76 | ||
|
|
2062c4c3e9 | ||
|
|
fbc3d1aa73 | ||
|
|
ec6166c232 | ||
|
|
ee21c612d5 | ||
|
|
9226a05ad8 | ||
|
|
31d57de339 | ||
|
|
bd91d74de8 | ||
|
|
fba676c405 | ||
|
|
1b6a558a7c | ||
|
|
92d32922ef | ||
|
|
00072a55f9 | ||
|
|
fec330b76e | ||
|
|
8b0514d73d | ||
|
|
f578ee4963 | ||
|
|
b828d5426b | ||
|
|
80a4b493ab | ||
|
|
6eed8cb962 | ||
|
|
8722d14048 | ||
|
|
0987ef0fa8 | ||
|
|
592e5dbe44 | ||
|
|
764f051318 | ||
|
|
457b6b13cc | ||
|
|
55e0c3c7bf | ||
|
|
26492c5783 | ||
|
|
4c6a3b2e43 | ||
|
|
45b9f5411e | ||
|
|
1e76b550e8 | ||
|
|
ef6909da88 | ||
|
|
01f11fc095 | ||
|
|
4b9e68950a | ||
|
|
e0cf044d60 | ||
|
|
9387211ce8 | ||
|
|
1ef314d3bd | ||
|
|
72ed023645 | ||
|
|
f97fe7fce2 | ||
|
|
029d3b0a97 | ||
|
|
87d8d117c1 | ||
|
|
d7a703828a | ||
|
|
8fdc5ad97b | ||
|
|
70510f8410 |
@@ -1,5 +1 @@
|
||||
*pro.user
|
||||
|
||||
|
||||
**/.vs/
|
||||
build/
|
||||
**/xcuserdata/
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[submodule "moonlight-common-c/moonlight-common-c"]
|
||||
path = moonlight-common-c/moonlight-common-c
|
||||
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||
[submodule "qmdnsengine/qmdnsengine"]
|
||||
path = qmdnsengine/qmdnsengine
|
||||
url = https://github.com/cgutman/qmdnsengine.git
|
||||
[submodule "limelight-common-c"]
|
||||
path = limelight-common-c
|
||||
url = git@github.com:limelight-stream/limelight-common-c.git
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
language: cpp
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
compiler: clang
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: xenial
|
||||
compiler: gcc
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
||||
packages:
|
||||
- qt59base
|
||||
- qt59quickcontrols2
|
||||
- qt59svg
|
||||
- libesd0-dev
|
||||
- libgl-dev
|
||||
- libopus-dev
|
||||
|
||||
install:
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
|
||||
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
|
||||
|
||||
before_script:
|
||||
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
||||
- qmake -v
|
||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
|
||||
|
||||
script:
|
||||
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
||||
- make debug
|
||||
- make release
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/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"
|
||||
|
||||
cd $OPUS_DIR
|
||||
|
||||
DIST_DIR=$DIST_DIR_BASE-$ARCH
|
||||
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"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
|
||||
IOSSDK=iPhoneOS${IOSSDK_VER}
|
||||
;;
|
||||
armv7s)
|
||||
EXTRA_FLAGS="--with-pic --enable-fixed-point"
|
||||
EXTRA_CFLAGS="-mcpu=cortex-a9 -mfpu=neon -miphoneos-version-min=6.0"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
|
||||
IOSSDK=iPhoneOS${IOSSDK_VER}
|
||||
;;
|
||||
aarch64)
|
||||
CFLAGS_ARCH="arm64"
|
||||
EXTRA_FLAGS="--with-pic --enable-fixed-point"
|
||||
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
|
||||
IOSSDK=iPhoneOS${IOSSDK_VER}
|
||||
;;
|
||||
x86_64)
|
||||
EXTRA_FLAGS="--with-pic"
|
||||
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneSimulator.platform"
|
||||
IOSSDK=iPhoneSimulator${IOSSDK_VER}
|
||||
;;
|
||||
i386)
|
||||
EXTRA_FLAGS="--with-pic"
|
||||
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
|
||||
PLATFORM="${PLATFORMBASE}/iPhoneSimulator.platform"
|
||||
IOSSDK=iPhoneSimulator${IOSSDK_VER}
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture ${ARCH}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Configuring opus for $ARCH..."
|
||||
|
||||
./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"
|
||||
|
||||
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"
|
||||
|
||||
./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..."
|
||||
make clean
|
||||
make -j$NJOB V=1
|
||||
make install
|
||||
|
||||
cd $SCRIPT_DIR
|
||||
|
||||
if [ -d $DIST_DIR/bin ]
|
||||
then
|
||||
rm -rf $DIST_DIR/bin
|
||||
fi
|
||||
if [ -d $DIST_DIR/share ]
|
||||
then
|
||||
rm -rf $DIST_DIR/share
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
source config.sh
|
||||
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
|
||||
if [ -d dist-$ARCH ]
|
||||
then
|
||||
MAIN_ARCH=$ARCH
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MAIN_ARCH" ]
|
||||
then
|
||||
echo "Please compile an architecture"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
OUTPUT_DIR="dist-uarch"
|
||||
rm -rf $OUTPUT_DIR
|
||||
|
||||
mkdir -p $OUTPUT_DIR/lib $OUTPUT_DIR/include
|
||||
|
||||
for LIB in dist-$MAIN_ARCH/lib/*.a
|
||||
do
|
||||
LIB=`basename $LIB`
|
||||
LIPO_CREATE=""
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
LIPO_ARCH=$ARCH
|
||||
if [ "$ARCH" = "aarch64" ];
|
||||
then
|
||||
LIPO_ARCH="arm64"
|
||||
fi
|
||||
if [ -d dist-$ARCH ]
|
||||
then
|
||||
LIPO_CREATE="$LIPO_CREATE-arch $LIPO_ARCH dist-$ARCH/lib/$LIB "
|
||||
fi
|
||||
done
|
||||
OUTPUT="$OUTPUT_DIR/lib/$LIB"
|
||||
echo "Creating: $OUTPUT"
|
||||
xcrun -sdk iphoneos lipo -create $LIPO_CREATE -output $OUTPUT
|
||||
xcrun -sdk iphoneos lipo -info $OUTPUT
|
||||
done
|
||||
|
||||
echo "Copying headers from dist-$MAIN_ARCH..."
|
||||
cp -R dist-$MAIN_ARCH/include/* $OUTPUT_DIR/include
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
OPUS_DIR="opus"
|
||||
IOSSDK_VER=8.1
|
||||
ARCHS="armv7 armv7s aarch64 i386 x86_64"
|
||||
|
||||
remove_arch() {
|
||||
OLD_ARCHS="$ARCHS"
|
||||
NEW_ARCHS=""
|
||||
REMOVAL="$1"
|
||||
|
||||
for ARCH in $OLD_ARCHS; do
|
||||
if [ "$ARCH" != "$REMOVAL" ] ; then
|
||||
NEW_ARCHS="$NEW_ARCHS $ARCH"
|
||||
fi
|
||||
done
|
||||
|
||||
ARCHS=$NEW_ARCHS
|
||||
}
|
||||
|
||||
# armv7s is only supported in iOS 6.0+
|
||||
CHECK=`echo $IOSSDK_VER '>= 6.0' | bc -l`
|
||||
if [ "$CHECK" = "0" ] ; then
|
||||
remove_arch "armv7s"
|
||||
fi
|
||||
|
||||
# armv6 is not supported any more since iOS 6.0
|
||||
CHECK=`echo $IOSSDK_VER '< 6.0' | bc -l`
|
||||
if [ "$CHECK" = "0" ] ; then
|
||||
remove_arch "armv6"
|
||||
fi
|
||||
|
||||
echo 'Architectures to build:' $ARCHS
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
source config.sh
|
||||
|
||||
SCRIPT_DIR=$( (cd -P $(dirname $0) && pwd) )
|
||||
DIST_DIR_BASE=${DIST_DIR_BASE:="$SCRIPT_DIR/dist"}
|
||||
|
||||
git submodule update --init opus
|
||||
cd opus
|
||||
git checkout master
|
||||
git pull origin master
|
||||
cd ..
|
||||
|
||||
for ARCH in $ARCHS
|
||||
do
|
||||
OPUS_DIR=opus-$ARCH
|
||||
echo "Syncing source for $ARCH to directory $OPUS_DIR"
|
||||
rsync opus/ $OPUS_DIR/ --exclude '.*' -a --delete
|
||||
done
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
SYSROOT_iPHONE="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk"
|
||||
SYSROOT_SIMULATOR="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
|
||||
CC_IPHONE="xcrun -sdk iphoneos clang"
|
||||
CC_SIMULATOR="xcrun -sdk iphonesimulator clang"
|
||||
function build_one {
|
||||
./configure \
|
||||
--prefix=$PREFIX \
|
||||
--disable-ffmpeg \
|
||||
--disable-ffplay \
|
||||
--disable-ffprobe \
|
||||
--disable-ffserver \
|
||||
--disable-armv5te \
|
||||
--disable-armv6 \
|
||||
--disable-doc \
|
||||
--disable-everything \
|
||||
--disable-debug \
|
||||
--enable-decoder=h264 \
|
||||
--enable-avresample \
|
||||
--enable-cross-compile \
|
||||
--sysroot=$SYSROOT \
|
||||
--target-os=darwin \
|
||||
--cc="$CC" \
|
||||
--extra-cflags="$CFLAGS" \
|
||||
--extra-ldflags="$LDFLAGS" \
|
||||
--enable-pic \
|
||||
$ADDI_FLAGS
|
||||
make clean && make -j4 && make install
|
||||
}
|
||||
|
||||
|
||||
# armv7
|
||||
function build_armv7 {
|
||||
PREFIX="armv7"
|
||||
SYSROOT=$SYSROOT_iPHONE
|
||||
CC=$CC_IPHONE
|
||||
CFLAGS="-arch armv7 -mfpu=neon -miphoneos-version-min=7.1 -fpic"
|
||||
LDFLAGS="-arch armv7 -isysroot $SYSROOT_iPHONE -miphoneos-version-min=7.1"
|
||||
ADDI_FLAGS="--arch=arm --cpu=cortex-a9"
|
||||
build_one
|
||||
}
|
||||
|
||||
# armv7s
|
||||
function build_armv7s {
|
||||
PREFIX="armv7s"
|
||||
SYSROOT=$SYSROOT_iPHONE
|
||||
CC=$CC_IPHONE
|
||||
CFLAGS="-arch armv7s -mfpu=neon -miphoneos-version-min=7.1"
|
||||
LDFLAGS="-arch armv7s -isysroot $SYSROOT_iPHONE -miphoneos-version-min=7.1"
|
||||
ADDI_FLAGS="--arch=arm --cpu=cortex-a9"
|
||||
build_one
|
||||
}
|
||||
|
||||
# i386
|
||||
function build_i386 {
|
||||
PREFIX="i386"
|
||||
SYSROOT=$SYSROOT_SIMULATOR
|
||||
CC=$CC_SIMULATOR
|
||||
CFLAGS="-arch i386"
|
||||
LDFLAGS="-arch i386 -isysroot $SYSROOT_SIMULATOR -mios-simulator-version-min=7.1"
|
||||
ADDI_FLAGS="--arch=i386 --cpu=i386 --disable-asm"
|
||||
build_one
|
||||
}
|
||||
|
||||
|
||||
# create fat library
|
||||
function build_universal {
|
||||
cd armv7/lib
|
||||
for file in *.a
|
||||
do
|
||||
cd ../..
|
||||
xcrun -sdk iphoneos lipo -output universal/lib/$file -create \
|
||||
-arch armv7 armv7/lib/$file \
|
||||
-arch armv7s armv7s/lib/$file \
|
||||
-arch i386 i386/lib/$file
|
||||
echo "Universal $file created."
|
||||
cd -
|
||||
done
|
||||
cd ../..
|
||||
}
|
||||
|
||||
echo "FFmpeg directory?"
|
||||
read SOURCE
|
||||
|
||||
cd $SOURCE
|
||||
|
||||
if [ "$1" = "clean" ]
|
||||
then
|
||||
rm -r armv7 armv7s i386 universal
|
||||
make clean
|
||||
exit 0
|
||||
fi
|
||||
mkdir armv7
|
||||
mkdir armv7s
|
||||
mkdir i386
|
||||
mkdir -p universal/lib
|
||||
|
||||
build_armv7
|
||||
build_armv7s
|
||||
build_i386
|
||||
build_universal
|
||||
echo "Ouput files in $SOURCE/armv7 $SOURCE/armv7s $SOURCE/i386 $SOURCE/universal"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{one line to give the program's name and a brief idea of what it does.}
|
||||
Copyright (C) {year} {name of author}
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
{project} Copyright (C) {year} {fullname}
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2014 Limelight Stream. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
|
||||
<rect key="frame" x="22" y="439" width="437" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.6705882352941176" green="0.83137254901960789" blue="0.086274509803921567" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<variation key="widthClass=compact">
|
||||
<fontDescription key="fontDescription" type="system" pointSize="11"/>
|
||||
</variation>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="store_icon_1024x1024" translatesAutoresizingMaskIntoConstraints="NO" id="tIx-KE-YER">
|
||||
<rect key="frame" x="127" y="126" width="227" height="227"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="227" id="EI7-Sf-3cH"/>
|
||||
<constraint firstAttribute="height" constant="227" id="b0G-y9-aP0"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="tIx-KE-YER" firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="GN9-Xb-RLD"/>
|
||||
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Gnz-uJ-CDw"/>
|
||||
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9"/>
|
||||
<constraint firstAttribute="centerY" secondItem="tIx-KE-YER" secondAttribute="centerY" constant="0.5" id="trN-ZP-0Hg"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="404" y="445"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="store_icon_1024x1024" width="1024" height="1024"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,975 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
98A03B4D19F352EB00861ACA /* liblimelight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98A03B4A19F3514B00861ACA /* liblimelight-common.a */; };
|
||||
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF119B2C406004C83CF /* Foundation.framework */; };
|
||||
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF319B2C406004C83CF /* CoreGraphics.framework */; };
|
||||
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF519B2C406004C83CF /* UIKit.framework */; };
|
||||
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF719B2C406004C83CF /* CoreData.framework */; };
|
||||
FB290CFE19B2C406004C83CF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FB290CFC19B2C406004C83CF /* InfoPlist.strings */; };
|
||||
FB290D0019B2C406004C83CF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FB290CFF19B2C406004C83CF /* main.m */; };
|
||||
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 */; };
|
||||
FB290D1119B2C406004C83CF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF119B2C406004C83CF /* Foundation.framework */; };
|
||||
FB290D1219B2C406004C83CF /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF519B2C406004C83CF /* UIKit.framework */; };
|
||||
FB290D1319B2C406004C83CF /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB290CF719B2C406004C83CF /* CoreData.framework */; };
|
||||
FB290D1B19B2C406004C83CF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FB290D1919B2C406004C83CF /* InfoPlist.strings */; };
|
||||
FB290D1D19B2C406004C83CF /* LimelightTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FB290D1C19B2C406004C83CF /* LimelightTests.m */; };
|
||||
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 /* MainFrame-iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB290E7819B37D81004C83CF /* MainFrame-iPad.storyboard */; };
|
||||
FB290E7B19B38036004C83CF /* MainFrame-iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB290E7A19B38036004C83CF /* MainFrame-iPhone.storyboard */; };
|
||||
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 */; };
|
||||
FB89462B19F646E200339C8A /* StreamView.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89460D19F646E200339C8A /* StreamView.m */; };
|
||||
FB89462C19F646E200339C8A /* HttpManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461019F646E200339C8A /* HttpManager.m */; };
|
||||
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461219F646E200339C8A /* MDNSManager.m */; };
|
||||
FB89462E19F646E200339C8A /* PairManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461419F646E200339C8A /* PairManager.m */; };
|
||||
FB89462F19F646E200339C8A /* Connection.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461719F646E200339C8A /* Connection.m */; };
|
||||
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461919F646E200339C8A /* StreamConfiguration.m */; };
|
||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461B19F646E200339C8A /* StreamManager.m */; };
|
||||
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */; };
|
||||
FB89463319F646E200339C8A /* Computer.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462019F646E200339C8A /* Computer.m */; };
|
||||
FB89463419F646E200339C8A /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462219F646E200339C8A /* Utils.m */; };
|
||||
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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
98A03B4919F3514B00861ACA /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 98A03B4519F3514B00861ACA /* limelight-common.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = FB290E2E19B37A4E004C83CF;
|
||||
remoteInfo = "limelight-common";
|
||||
};
|
||||
98A03B4B19F3517100861ACA /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 98A03B4519F3514B00861ACA /* limelight-common.xcodeproj */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = FB290E2D19B37A4E004C83CF;
|
||||
remoteInfo = "limelight-common";
|
||||
};
|
||||
FB290D1419B2C406004C83CF /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = FB290CE619B2C406004C83CF /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = FB290CED19B2C406004C83CF;
|
||||
remoteInfo = Limelight;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
98A03B4519F3514B00861ACA /* limelight-common.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "limelight-common.xcodeproj"; path = "limelight-common-c/limelight-common.xcodeproj"; sourceTree = "<group>"; };
|
||||
FB290CEE19B2C406004C83CF /* Limelight.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Limelight.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FB290CF119B2C406004C83CF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
|
||||
FB290CF319B2C406004C83CF /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||
FB290CF519B2C406004C83CF /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
FB290CF719B2C406004C83CF /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
|
||||
FB290CFB19B2C406004C83CF /* Limelight-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Limelight-Info.plist"; sourceTree = "<group>"; };
|
||||
FB290CFD19B2C406004C83CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
FB290CFF19B2C406004C83CF /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
FB290D0119B2C406004C83CF /* Limelight-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Limelight-Prefix.pch"; sourceTree = "<group>"; };
|
||||
FB290D0219B2C406004C83CF /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||
FB290D0319B2C406004C83CF /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||
FB290D0619B2C406004C83CF /* Limelight.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Limelight.xcdatamodel; sourceTree = "<group>"; };
|
||||
FB290D0819B2C406004C83CF /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
|
||||
FB290D0E19B2C406004C83CF /* LimelightTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LimelightTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FB290D1819B2C406004C83CF /* LimelightTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "LimelightTests-Info.plist"; sourceTree = "<group>"; };
|
||||
FB290D1A19B2C406004C83CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
FB290D1C19B2C406004C83CF /* LimelightTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LimelightTests.m; sourceTree = "<group>"; };
|
||||
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 /* MainFrame-iPad.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "MainFrame-iPad.storyboard"; sourceTree = SOURCE_ROOT; };
|
||||
FB290E7A19B38036004C83CF /* MainFrame-iPhone.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "MainFrame-iPhone.storyboard"; sourceTree = SOURCE_ROOT; };
|
||||
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 = "<group>"; };
|
||||
FB89460619F646E200339C8A /* CryptoManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CryptoManager.m; sourceTree = "<group>"; };
|
||||
FB89460719F646E200339C8A /* mkcert.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mkcert.c; sourceTree = "<group>"; };
|
||||
FB89460819F646E200339C8A /* mkcert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mkcert.h; sourceTree = "<group>"; };
|
||||
FB89460A19F646E200339C8A /* ControllerSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ControllerSupport.h; sourceTree = "<group>"; };
|
||||
FB89460B19F646E200339C8A /* ControllerSupport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ControllerSupport.m; sourceTree = "<group>"; };
|
||||
FB89460C19F646E200339C8A /* StreamView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamView.h; sourceTree = "<group>"; };
|
||||
FB89460D19F646E200339C8A /* StreamView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamView.m; sourceTree = "<group>"; };
|
||||
FB89460F19F646E200339C8A /* HttpManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HttpManager.h; sourceTree = "<group>"; };
|
||||
FB89461019F646E200339C8A /* HttpManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HttpManager.m; sourceTree = "<group>"; };
|
||||
FB89461119F646E200339C8A /* MDNSManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MDNSManager.h; sourceTree = "<group>"; };
|
||||
FB89461219F646E200339C8A /* MDNSManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MDNSManager.m; sourceTree = "<group>"; };
|
||||
FB89461319F646E200339C8A /* PairManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PairManager.h; sourceTree = "<group>"; };
|
||||
FB89461419F646E200339C8A /* PairManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PairManager.m; sourceTree = "<group>"; };
|
||||
FB89461619F646E200339C8A /* Connection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Connection.h; sourceTree = "<group>"; };
|
||||
FB89461719F646E200339C8A /* Connection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Connection.m; sourceTree = "<group>"; };
|
||||
FB89461819F646E200339C8A /* StreamConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamConfiguration.h; sourceTree = "<group>"; };
|
||||
FB89461919F646E200339C8A /* StreamConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamConfiguration.m; sourceTree = "<group>"; };
|
||||
FB89461A19F646E200339C8A /* StreamManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamManager.h; sourceTree = "<group>"; };
|
||||
FB89461B19F646E200339C8A /* StreamManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamManager.m; sourceTree = "<group>"; };
|
||||
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoDecoderRenderer.h; sourceTree = "<group>"; };
|
||||
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VideoDecoderRenderer.m; sourceTree = "<group>"; };
|
||||
FB89461F19F646E200339C8A /* Computer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Computer.h; sourceTree = "<group>"; };
|
||||
FB89462019F646E200339C8A /* Computer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Computer.m; sourceTree = "<group>"; };
|
||||
FB89462119F646E200339C8A /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utils.h; sourceTree = "<group>"; };
|
||||
FB89462219F646E200339C8A /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utils.m; sourceTree = "<group>"; };
|
||||
FB89462419F646E200339C8A /* MainFrameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainFrameViewController.h; sourceTree = "<group>"; };
|
||||
FB89462519F646E200339C8A /* MainFrameViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainFrameViewController.m; sourceTree = "<group>"; };
|
||||
FB89462619F646E200339C8A /* StreamFrameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamFrameViewController.h; sourceTree = "<group>"; };
|
||||
FB89462719F646E200339C8A /* StreamFrameViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamFrameViewController.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
FB89469519F6AFB800339C8A /* asn1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1.h; sourceTree = "<group>"; };
|
||||
FB89469619F6AFB800339C8A /* asn1_mac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1_mac.h; sourceTree = "<group>"; };
|
||||
FB89469719F6AFB800339C8A /* asn1t.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1t.h; sourceTree = "<group>"; };
|
||||
FB89469819F6AFB800339C8A /* bio.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bio.h; sourceTree = "<group>"; };
|
||||
FB89469919F6AFB800339C8A /* blowfish.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = blowfish.h; sourceTree = "<group>"; };
|
||||
FB89469A19F6AFB800339C8A /* bn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bn.h; sourceTree = "<group>"; };
|
||||
FB89469B19F6AFB800339C8A /* buffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = buffer.h; sourceTree = "<group>"; };
|
||||
FB89469C19F6AFB800339C8A /* camellia.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = camellia.h; sourceTree = "<group>"; };
|
||||
FB89469D19F6AFB800339C8A /* cast.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cast.h; sourceTree = "<group>"; };
|
||||
FB89469E19F6AFB800339C8A /* cmac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cmac.h; sourceTree = "<group>"; };
|
||||
FB89469F19F6AFB800339C8A /* cms.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cms.h; sourceTree = "<group>"; };
|
||||
FB8946A019F6AFB800339C8A /* comp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = comp.h; sourceTree = "<group>"; };
|
||||
FB8946A119F6AFB800339C8A /* conf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = conf.h; sourceTree = "<group>"; };
|
||||
FB8946A219F6AFB800339C8A /* conf_api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = conf_api.h; sourceTree = "<group>"; };
|
||||
FB8946A319F6AFB800339C8A /* crypto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = crypto.h; sourceTree = "<group>"; };
|
||||
FB8946A419F6AFB800339C8A /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = des.h; sourceTree = "<group>"; };
|
||||
FB8946A519F6AFB800339C8A /* des_old.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = des_old.h; sourceTree = "<group>"; };
|
||||
FB8946A619F6AFB800339C8A /* dh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dh.h; sourceTree = "<group>"; };
|
||||
FB8946A719F6AFB800339C8A /* dsa.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dsa.h; sourceTree = "<group>"; };
|
||||
FB8946A819F6AFB800339C8A /* dso.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dso.h; sourceTree = "<group>"; };
|
||||
FB8946A919F6AFB800339C8A /* dtls1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dtls1.h; sourceTree = "<group>"; };
|
||||
FB8946AA19F6AFB800339C8A /* e_os2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = e_os2.h; sourceTree = "<group>"; };
|
||||
FB8946AB19F6AFB800339C8A /* ebcdic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ebcdic.h; sourceTree = "<group>"; };
|
||||
FB8946AC19F6AFB800339C8A /* ec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ec.h; sourceTree = "<group>"; };
|
||||
FB8946AD19F6AFB800339C8A /* ecdh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ecdh.h; sourceTree = "<group>"; };
|
||||
FB8946AE19F6AFB800339C8A /* ecdsa.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ecdsa.h; sourceTree = "<group>"; };
|
||||
FB8946AF19F6AFB800339C8A /* engine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = engine.h; sourceTree = "<group>"; };
|
||||
FB8946B019F6AFB800339C8A /* err.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = err.h; sourceTree = "<group>"; };
|
||||
FB8946B119F6AFB800339C8A /* evp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = evp.h; sourceTree = "<group>"; };
|
||||
FB8946B219F6AFB800339C8A /* hmac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hmac.h; sourceTree = "<group>"; };
|
||||
FB8946B319F6AFB800339C8A /* idea.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = idea.h; sourceTree = "<group>"; };
|
||||
FB8946B419F6AFB800339C8A /* krb5_asn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = krb5_asn.h; sourceTree = "<group>"; };
|
||||
FB8946B519F6AFB800339C8A /* kssl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = kssl.h; sourceTree = "<group>"; };
|
||||
FB8946B619F6AFB800339C8A /* lhash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = lhash.h; sourceTree = "<group>"; };
|
||||
FB8946B719F6AFB800339C8A /* md4.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = md4.h; sourceTree = "<group>"; };
|
||||
FB8946B819F6AFB800339C8A /* md5.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = md5.h; sourceTree = "<group>"; };
|
||||
FB8946B919F6AFB800339C8A /* mdc2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mdc2.h; sourceTree = "<group>"; };
|
||||
FB8946BA19F6AFB800339C8A /* modes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = modes.h; sourceTree = "<group>"; };
|
||||
FB8946BB19F6AFB800339C8A /* obj_mac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = obj_mac.h; sourceTree = "<group>"; };
|
||||
FB8946BC19F6AFB800339C8A /* objects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objects.h; sourceTree = "<group>"; };
|
||||
FB8946BD19F6AFB800339C8A /* ocsp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ocsp.h; sourceTree = "<group>"; };
|
||||
FB8946BE19F6AFB800339C8A /* opensslconf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf.h; sourceTree = "<group>"; };
|
||||
FB8946BF19F6AFB800339C8A /* opensslv.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslv.h; sourceTree = "<group>"; };
|
||||
FB8946C019F6AFB800339C8A /* ossl_typ.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ossl_typ.h; sourceTree = "<group>"; };
|
||||
FB8946C119F6AFB800339C8A /* pem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pem.h; sourceTree = "<group>"; };
|
||||
FB8946C219F6AFB800339C8A /* pem2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pem2.h; sourceTree = "<group>"; };
|
||||
FB8946C319F6AFB800339C8A /* pkcs12.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pkcs12.h; sourceTree = "<group>"; };
|
||||
FB8946C419F6AFB800339C8A /* pkcs7.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pkcs7.h; sourceTree = "<group>"; };
|
||||
FB8946C519F6AFB800339C8A /* pqueue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pqueue.h; sourceTree = "<group>"; };
|
||||
FB8946C619F6AFB800339C8A /* rand.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rand.h; sourceTree = "<group>"; };
|
||||
FB8946C719F6AFB800339C8A /* rc2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rc2.h; sourceTree = "<group>"; };
|
||||
FB8946C819F6AFB800339C8A /* rc4.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rc4.h; sourceTree = "<group>"; };
|
||||
FB8946C919F6AFB800339C8A /* ripemd.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ripemd.h; sourceTree = "<group>"; };
|
||||
FB8946CA19F6AFB800339C8A /* rsa.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rsa.h; sourceTree = "<group>"; };
|
||||
FB8946CB19F6AFB800339C8A /* safestack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = safestack.h; sourceTree = "<group>"; };
|
||||
FB8946CC19F6AFB800339C8A /* seed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = seed.h; sourceTree = "<group>"; };
|
||||
FB8946CD19F6AFB800339C8A /* sha.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sha.h; sourceTree = "<group>"; };
|
||||
FB8946CE19F6AFB800339C8A /* srp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = srp.h; sourceTree = "<group>"; };
|
||||
FB8946CF19F6AFB800339C8A /* srtp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = srtp.h; sourceTree = "<group>"; };
|
||||
FB8946D019F6AFB800339C8A /* ssl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl.h; sourceTree = "<group>"; };
|
||||
FB8946D119F6AFB800339C8A /* ssl2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl2.h; sourceTree = "<group>"; };
|
||||
FB8946D219F6AFB800339C8A /* ssl23.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl23.h; sourceTree = "<group>"; };
|
||||
FB8946D319F6AFB800339C8A /* ssl3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl3.h; sourceTree = "<group>"; };
|
||||
FB8946D419F6AFB800339C8A /* stack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = stack.h; sourceTree = "<group>"; };
|
||||
FB8946D519F6AFB800339C8A /* symhacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = symhacks.h; sourceTree = "<group>"; };
|
||||
FB8946D619F6AFB800339C8A /* tls1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = tls1.h; sourceTree = "<group>"; };
|
||||
FB8946D719F6AFB800339C8A /* ts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ts.h; sourceTree = "<group>"; };
|
||||
FB8946D819F6AFB800339C8A /* txt_db.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = txt_db.h; sourceTree = "<group>"; };
|
||||
FB8946D919F6AFB800339C8A /* ui.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ui.h; sourceTree = "<group>"; };
|
||||
FB8946DA19F6AFB800339C8A /* ui_compat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ui_compat.h; sourceTree = "<group>"; };
|
||||
FB8946DB19F6AFB800339C8A /* whrlpool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = whrlpool.h; sourceTree = "<group>"; };
|
||||
FB8946DC19F6AFB800339C8A /* x509.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x509.h; sourceTree = "<group>"; };
|
||||
FB8946DD19F6AFB800339C8A /* x509_vfy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x509_vfy.h; sourceTree = "<group>"; };
|
||||
FB8946DE19F6AFB800339C8A /* x509v3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x509v3.h; sourceTree = "<group>"; };
|
||||
FB8946E019F6AFB800339C8A /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libcrypto.a; sourceTree = "<group>"; };
|
||||
FB8946E119F6AFB800339C8A /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libssl.a; sourceTree = "<group>"; };
|
||||
FB8946E519F6AFB800339C8A /* opus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus.h; sourceTree = "<group>"; };
|
||||
FB8946E619F6AFB800339C8A /* opus_defines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_defines.h; sourceTree = "<group>"; };
|
||||
FB8946E719F6AFB800339C8A /* opus_multistream.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_multistream.h; sourceTree = "<group>"; };
|
||||
FB8946E819F6AFB800339C8A /* opus_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_types.h; sourceTree = "<group>"; };
|
||||
FB8946EA19F6AFB800339C8A /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libopus.a; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
FB290CEB19B2C406004C83CF /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
98A03B4D19F352EB00861ACA /* liblimelight-common.a in Frameworks */,
|
||||
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
|
||||
FB8946EB19F6AFE100339C8A /* libcrypto.a in Frameworks */,
|
||||
FB8946EC19F6AFE400339C8A /* libssl.a in Frameworks */,
|
||||
FB7E794419C8B71B00A15F68 /* libiconv.dylib in Frameworks */,
|
||||
FB290DC419B2E98F004C83CF /* libxml2.dylib in Frameworks */,
|
||||
FB290DB919B2C877004C83CF /* libbz2.dylib in Frameworks */,
|
||||
FB290DB719B2C870004C83CF /* libz.dylib in Frameworks */,
|
||||
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
|
||||
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
|
||||
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
|
||||
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
FB290D0B19B2C406004C83CF /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FB290D1319B2C406004C83CF /* CoreData.framework in Frameworks */,
|
||||
FB290D1219B2C406004C83CF /* UIKit.framework in Frameworks */,
|
||||
FB290D1119B2C406004C83CF /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
98A03B4619F3514B00861ACA /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98A03B4A19F3514B00861ACA /* liblimelight-common.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290CE519B2C406004C83CF = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98A03B4519F3514B00861ACA /* limelight-common.xcodeproj */,
|
||||
FB290CF919B2C406004C83CF /* Limelight */,
|
||||
FB290D1619B2C406004C83CF /* LimelightTests */,
|
||||
FB290CF019B2C406004C83CF /* Frameworks */,
|
||||
FB290CEF19B2C406004C83CF /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290CEF19B2C406004C83CF /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB290CEE19B2C406004C83CF /* Limelight.app */,
|
||||
FB290D0E19B2C406004C83CF /* LimelightTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290CF019B2C406004C83CF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89468F19F6AFB800339C8A /* libs */,
|
||||
FB7E794319C8B71B00A15F68 /* libiconv.dylib */,
|
||||
FB290DC319B2E98F004C83CF /* libxml2.dylib */,
|
||||
FB290DB819B2C877004C83CF /* libbz2.dylib */,
|
||||
FB290DB619B2C870004C83CF /* libz.dylib */,
|
||||
FB290CF119B2C406004C83CF /* Foundation.framework */,
|
||||
FB290CF319B2C406004C83CF /* CoreGraphics.framework */,
|
||||
FB290CF519B2C406004C83CF /* UIKit.framework */,
|
||||
FB290CF719B2C406004C83CF /* CoreData.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290CF919B2C406004C83CF /* Limelight */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89460419F646E200339C8A /* Crypto */,
|
||||
FB89460919F646E200339C8A /* Input */,
|
||||
FB89460E19F646E200339C8A /* Network */,
|
||||
FB89461519F646E200339C8A /* Stream */,
|
||||
FB89461E19F646E200339C8A /* Utility */,
|
||||
FB89462319F646E200339C8A /* ViewControllers */,
|
||||
FB290CFA19B2C406004C83CF /* Supporting Files */,
|
||||
FB290D0219B2C406004C83CF /* AppDelegate.h */,
|
||||
FB290D0319B2C406004C83CF /* AppDelegate.m */,
|
||||
FB290E7819B37D81004C83CF /* MainFrame-iPad.storyboard */,
|
||||
FB290E7A19B38036004C83CF /* MainFrame-iPhone.storyboard */,
|
||||
FB89463719F6473800339C8A /* Launch Screen.xib */,
|
||||
FB290D0819B2C406004C83CF /* Images.xcassets */,
|
||||
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */,
|
||||
);
|
||||
path = Limelight;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290CFA19B2C406004C83CF /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB290CFB19B2C406004C83CF /* Limelight-Info.plist */,
|
||||
FB290CFC19B2C406004C83CF /* InfoPlist.strings */,
|
||||
FB290CFF19B2C406004C83CF /* main.m */,
|
||||
FB290D0119B2C406004C83CF /* Limelight-Prefix.pch */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290D1619B2C406004C83CF /* LimelightTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB290D1C19B2C406004C83CF /* LimelightTests.m */,
|
||||
FB290D1719B2C406004C83CF /* Supporting Files */,
|
||||
);
|
||||
path = LimelightTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290D1719B2C406004C83CF /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB290D1819B2C406004C83CF /* LimelightTests-Info.plist */,
|
||||
FB290D1919B2C406004C83CF /* InfoPlist.strings */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89460419F646E200339C8A /* Crypto */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89460519F646E200339C8A /* CryptoManager.h */,
|
||||
FB89460619F646E200339C8A /* CryptoManager.m */,
|
||||
FB89460719F646E200339C8A /* mkcert.c */,
|
||||
FB89460819F646E200339C8A /* mkcert.h */,
|
||||
);
|
||||
path = Crypto;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89460919F646E200339C8A /* Input */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89460A19F646E200339C8A /* ControllerSupport.h */,
|
||||
FB89460B19F646E200339C8A /* ControllerSupport.m */,
|
||||
FB89460C19F646E200339C8A /* StreamView.h */,
|
||||
FB89460D19F646E200339C8A /* StreamView.m */,
|
||||
);
|
||||
path = Input;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89460E19F646E200339C8A /* Network */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89460F19F646E200339C8A /* HttpManager.h */,
|
||||
FB89461019F646E200339C8A /* HttpManager.m */,
|
||||
FB89461119F646E200339C8A /* MDNSManager.h */,
|
||||
FB89461219F646E200339C8A /* MDNSManager.m */,
|
||||
FB89461319F646E200339C8A /* PairManager.h */,
|
||||
FB89461419F646E200339C8A /* PairManager.m */,
|
||||
);
|
||||
path = Network;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89461519F646E200339C8A /* Stream */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89461619F646E200339C8A /* Connection.h */,
|
||||
FB89461719F646E200339C8A /* Connection.m */,
|
||||
FB89461819F646E200339C8A /* StreamConfiguration.h */,
|
||||
FB89461919F646E200339C8A /* StreamConfiguration.m */,
|
||||
FB89461A19F646E200339C8A /* StreamManager.h */,
|
||||
FB89461B19F646E200339C8A /* StreamManager.m */,
|
||||
FB89461C19F646E200339C8A /* VideoDecoderRenderer.h */,
|
||||
FB89461D19F646E200339C8A /* VideoDecoderRenderer.m */,
|
||||
);
|
||||
path = Stream;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89461E19F646E200339C8A /* Utility */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89461F19F646E200339C8A /* Computer.h */,
|
||||
FB89462019F646E200339C8A /* Computer.m */,
|
||||
FB89462119F646E200339C8A /* Utils.h */,
|
||||
FB89462219F646E200339C8A /* Utils.m */,
|
||||
);
|
||||
path = Utility;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89462319F646E200339C8A /* ViewControllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89462419F646E200339C8A /* MainFrameViewController.h */,
|
||||
FB89462519F646E200339C8A /* MainFrameViewController.m */,
|
||||
FB89462619F646E200339C8A /* StreamFrameViewController.h */,
|
||||
FB89462719F646E200339C8A /* StreamFrameViewController.m */,
|
||||
);
|
||||
path = ViewControllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89468F19F6AFB800339C8A /* libs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89469019F6AFB800339C8A /* limelight-common */,
|
||||
FB89469119F6AFB800339C8A /* openssl */,
|
||||
FB8946E219F6AFB800339C8A /* opus */,
|
||||
);
|
||||
path = libs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89469019F6AFB800339C8A /* limelight-common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = "limelight-common";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89469119F6AFB800339C8A /* openssl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89469219F6AFB800339C8A /* include */,
|
||||
FB8946DF19F6AFB800339C8A /* lib */,
|
||||
);
|
||||
path = openssl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89469219F6AFB800339C8A /* include */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89469319F6AFB800339C8A /* openssl */,
|
||||
);
|
||||
path = include;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB89469319F6AFB800339C8A /* openssl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB89469419F6AFB800339C8A /* aes.h */,
|
||||
FB89469519F6AFB800339C8A /* asn1.h */,
|
||||
FB89469619F6AFB800339C8A /* asn1_mac.h */,
|
||||
FB89469719F6AFB800339C8A /* asn1t.h */,
|
||||
FB89469819F6AFB800339C8A /* bio.h */,
|
||||
FB89469919F6AFB800339C8A /* blowfish.h */,
|
||||
FB89469A19F6AFB800339C8A /* bn.h */,
|
||||
FB89469B19F6AFB800339C8A /* buffer.h */,
|
||||
FB89469C19F6AFB800339C8A /* camellia.h */,
|
||||
FB89469D19F6AFB800339C8A /* cast.h */,
|
||||
FB89469E19F6AFB800339C8A /* cmac.h */,
|
||||
FB89469F19F6AFB800339C8A /* cms.h */,
|
||||
FB8946A019F6AFB800339C8A /* comp.h */,
|
||||
FB8946A119F6AFB800339C8A /* conf.h */,
|
||||
FB8946A219F6AFB800339C8A /* conf_api.h */,
|
||||
FB8946A319F6AFB800339C8A /* crypto.h */,
|
||||
FB8946A419F6AFB800339C8A /* des.h */,
|
||||
FB8946A519F6AFB800339C8A /* des_old.h */,
|
||||
FB8946A619F6AFB800339C8A /* dh.h */,
|
||||
FB8946A719F6AFB800339C8A /* dsa.h */,
|
||||
FB8946A819F6AFB800339C8A /* dso.h */,
|
||||
FB8946A919F6AFB800339C8A /* dtls1.h */,
|
||||
FB8946AA19F6AFB800339C8A /* e_os2.h */,
|
||||
FB8946AB19F6AFB800339C8A /* ebcdic.h */,
|
||||
FB8946AC19F6AFB800339C8A /* ec.h */,
|
||||
FB8946AD19F6AFB800339C8A /* ecdh.h */,
|
||||
FB8946AE19F6AFB800339C8A /* ecdsa.h */,
|
||||
FB8946AF19F6AFB800339C8A /* engine.h */,
|
||||
FB8946B019F6AFB800339C8A /* err.h */,
|
||||
FB8946B119F6AFB800339C8A /* evp.h */,
|
||||
FB8946B219F6AFB800339C8A /* hmac.h */,
|
||||
FB8946B319F6AFB800339C8A /* idea.h */,
|
||||
FB8946B419F6AFB800339C8A /* krb5_asn.h */,
|
||||
FB8946B519F6AFB800339C8A /* kssl.h */,
|
||||
FB8946B619F6AFB800339C8A /* lhash.h */,
|
||||
FB8946B719F6AFB800339C8A /* md4.h */,
|
||||
FB8946B819F6AFB800339C8A /* md5.h */,
|
||||
FB8946B919F6AFB800339C8A /* mdc2.h */,
|
||||
FB8946BA19F6AFB800339C8A /* modes.h */,
|
||||
FB8946BB19F6AFB800339C8A /* obj_mac.h */,
|
||||
FB8946BC19F6AFB800339C8A /* objects.h */,
|
||||
FB8946BD19F6AFB800339C8A /* ocsp.h */,
|
||||
FB8946BE19F6AFB800339C8A /* opensslconf.h */,
|
||||
FB8946BF19F6AFB800339C8A /* opensslv.h */,
|
||||
FB8946C019F6AFB800339C8A /* ossl_typ.h */,
|
||||
FB8946C119F6AFB800339C8A /* pem.h */,
|
||||
FB8946C219F6AFB800339C8A /* pem2.h */,
|
||||
FB8946C319F6AFB800339C8A /* pkcs12.h */,
|
||||
FB8946C419F6AFB800339C8A /* pkcs7.h */,
|
||||
FB8946C519F6AFB800339C8A /* pqueue.h */,
|
||||
FB8946C619F6AFB800339C8A /* rand.h */,
|
||||
FB8946C719F6AFB800339C8A /* rc2.h */,
|
||||
FB8946C819F6AFB800339C8A /* rc4.h */,
|
||||
FB8946C919F6AFB800339C8A /* ripemd.h */,
|
||||
FB8946CA19F6AFB800339C8A /* rsa.h */,
|
||||
FB8946CB19F6AFB800339C8A /* safestack.h */,
|
||||
FB8946CC19F6AFB800339C8A /* seed.h */,
|
||||
FB8946CD19F6AFB800339C8A /* sha.h */,
|
||||
FB8946CE19F6AFB800339C8A /* srp.h */,
|
||||
FB8946CF19F6AFB800339C8A /* srtp.h */,
|
||||
FB8946D019F6AFB800339C8A /* ssl.h */,
|
||||
FB8946D119F6AFB800339C8A /* ssl2.h */,
|
||||
FB8946D219F6AFB800339C8A /* ssl23.h */,
|
||||
FB8946D319F6AFB800339C8A /* ssl3.h */,
|
||||
FB8946D419F6AFB800339C8A /* stack.h */,
|
||||
FB8946D519F6AFB800339C8A /* symhacks.h */,
|
||||
FB8946D619F6AFB800339C8A /* tls1.h */,
|
||||
FB8946D719F6AFB800339C8A /* ts.h */,
|
||||
FB8946D819F6AFB800339C8A /* txt_db.h */,
|
||||
FB8946D919F6AFB800339C8A /* ui.h */,
|
||||
FB8946DA19F6AFB800339C8A /* ui_compat.h */,
|
||||
FB8946DB19F6AFB800339C8A /* whrlpool.h */,
|
||||
FB8946DC19F6AFB800339C8A /* x509.h */,
|
||||
FB8946DD19F6AFB800339C8A /* x509_vfy.h */,
|
||||
FB8946DE19F6AFB800339C8A /* x509v3.h */,
|
||||
);
|
||||
path = openssl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB8946DF19F6AFB800339C8A /* lib */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB8946E019F6AFB800339C8A /* libcrypto.a */,
|
||||
FB8946E119F6AFB800339C8A /* libssl.a */,
|
||||
);
|
||||
path = lib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB8946E219F6AFB800339C8A /* opus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB8946E319F6AFB800339C8A /* include */,
|
||||
FB8946E919F6AFB800339C8A /* lib */,
|
||||
);
|
||||
path = opus;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB8946E319F6AFB800339C8A /* include */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB8946E419F6AFB800339C8A /* opus */,
|
||||
);
|
||||
path = include;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB8946E419F6AFB800339C8A /* opus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB8946E519F6AFB800339C8A /* opus.h */,
|
||||
FB8946E619F6AFB800339C8A /* opus_defines.h */,
|
||||
FB8946E719F6AFB800339C8A /* opus_multistream.h */,
|
||||
FB8946E819F6AFB800339C8A /* opus_types.h */,
|
||||
);
|
||||
path = opus;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB8946E919F6AFB800339C8A /* lib */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FB8946EA19F6AFB800339C8A /* libopus.a */,
|
||||
);
|
||||
path = lib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
FB290CED19B2C406004C83CF /* Limelight */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "Limelight" */;
|
||||
buildPhases = (
|
||||
FB290CEA19B2C406004C83CF /* Sources */,
|
||||
FB290CEB19B2C406004C83CF /* Frameworks */,
|
||||
FB290CEC19B2C406004C83CF /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
98A03B4C19F3517100861ACA /* PBXTargetDependency */,
|
||||
);
|
||||
name = Limelight;
|
||||
productName = Limelight;
|
||||
productReference = FB290CEE19B2C406004C83CF /* Limelight.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
FB290D0D19B2C406004C83CF /* LimelightTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = FB290D2319B2C406004C83CF /* Build configuration list for PBXNativeTarget "LimelightTests" */;
|
||||
buildPhases = (
|
||||
FB290D0A19B2C406004C83CF /* Sources */,
|
||||
FB290D0B19B2C406004C83CF /* Frameworks */,
|
||||
FB290D0C19B2C406004C83CF /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
FB290D1519B2C406004C83CF /* PBXTargetDependency */,
|
||||
);
|
||||
name = LimelightTests;
|
||||
productName = LimelightTests;
|
||||
productReference = FB290D0E19B2C406004C83CF /* LimelightTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
FB290CE619B2C406004C83CF /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0510;
|
||||
ORGANIZATIONNAME = "Limelight Stream";
|
||||
TargetAttributes = {
|
||||
FB290CED19B2C406004C83CF = {
|
||||
DevelopmentTeam = DM46QST4M7;
|
||||
};
|
||||
FB290D0D19B2C406004C83CF = {
|
||||
TestTargetID = FB290CED19B2C406004C83CF;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "Limelight" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
);
|
||||
mainGroup = FB290CE519B2C406004C83CF;
|
||||
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
|
||||
projectDirPath = "";
|
||||
projectReferences = (
|
||||
{
|
||||
ProductGroup = 98A03B4619F3514B00861ACA /* Products */;
|
||||
ProjectRef = 98A03B4519F3514B00861ACA /* limelight-common.xcodeproj */;
|
||||
},
|
||||
);
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
FB290CED19B2C406004C83CF /* Limelight */,
|
||||
FB290D0D19B2C406004C83CF /* LimelightTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXReferenceProxy section */
|
||||
98A03B4A19F3514B00861ACA /* liblimelight-common.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "liblimelight-common.a";
|
||||
remoteRef = 98A03B4919F3514B00861ACA /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
/* End PBXReferenceProxy section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
FB290CEC19B2C406004C83CF /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FB290E7919B37D81004C83CF /* MainFrame-iPad.storyboard in Resources */,
|
||||
FB290CFE19B2C406004C83CF /* InfoPlist.strings in Resources */,
|
||||
FB290E7B19B38036004C83CF /* MainFrame-iPhone.storyboard in Resources */,
|
||||
FB290D0919B2C406004C83CF /* Images.xcassets in Resources */,
|
||||
FB89463819F6473800339C8A /* Launch Screen.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
FB290D0C19B2C406004C83CF /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FB290D1B19B2C406004C83CF /* InfoPlist.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
FB290CEA19B2C406004C83CF /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FB290D0719B2C406004C83CF /* Limelight.xcdatamodeld in Sources */,
|
||||
FB89463219F646E200339C8A /* VideoDecoderRenderer.m in Sources */,
|
||||
FB290D0419B2C406004C83CF /* AppDelegate.m in Sources */,
|
||||
FB89463419F646E200339C8A /* Utils.m in Sources */,
|
||||
FB89463319F646E200339C8A /* Computer.m in Sources */,
|
||||
FB89462F19F646E200339C8A /* Connection.m in Sources */,
|
||||
FB89462919F646E200339C8A /* mkcert.c in Sources */,
|
||||
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
|
||||
FB89462C19F646E200339C8A /* HttpManager.m in Sources */,
|
||||
FB89462D19F646E200339C8A /* MDNSManager.m in Sources */,
|
||||
FB89462B19F646E200339C8A /* StreamView.m in Sources */,
|
||||
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */,
|
||||
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */,
|
||||
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
|
||||
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
|
||||
FB290D0019B2C406004C83CF /* main.m in Sources */,
|
||||
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
|
||||
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
FB290D0A19B2C406004C83CF /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FB290D1D19B2C406004C83CF /* LimelightTests.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
98A03B4C19F3517100861ACA /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = "limelight-common";
|
||||
targetProxy = 98A03B4B19F3517100861ACA /* PBXContainerItemProxy */;
|
||||
};
|
||||
FB290D1519B2C406004C83CF /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = FB290CED19B2C406004C83CF /* Limelight */;
|
||||
targetProxy = FB290D1419B2C406004C83CF /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
FB290CFC19B2C406004C83CF /* InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
FB290CFD19B2C406004C83CF /* en */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FB290D1919B2C406004C83CF /* InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
FB290D1A19B2C406004C83CF /* en */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
FB290D1E19B2C406004C83CF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FB290D1F19B2C406004C83CF /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
PROVISIONING_PROFILE = "";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
FB290D2119B2C406004C83CF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(PROJECT_DIR)/limelight-common-c/limelight-common",
|
||||
"$(SDKROOT)/usr/include/libxml2/**",
|
||||
"$(PROJECT_DIR)/libs/**",
|
||||
);
|
||||
INFOPLIST_FILE = "Limelight/Limelight-Info.plist";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/libs/opus/lib",
|
||||
"$(PROJECT_DIR)/libs/openssl/lib",
|
||||
);
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
SKIP_INSTALL = NO;
|
||||
WRAPPER_EXTENSION = app;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FB290D2219B2C406004C83CF /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch";
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(PROJECT_DIR)/limelight-common-c/limelight-common",
|
||||
"$(SDKROOT)/usr/include/libxml2/**",
|
||||
"$(PROJECT_DIR)/libs/**",
|
||||
);
|
||||
INFOPLIST_FILE = "Limelight/Limelight-Info.plist";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/libs/opus/lib",
|
||||
"$(PROJECT_DIR)/libs/openssl/lib",
|
||||
);
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
SKIP_INSTALL = NO;
|
||||
WRAPPER_EXTENSION = app;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
FB290D2419B2C406004C83CF /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Limelight.app/Limelight";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
"$(DEVELOPER_FRAMEWORKS_DIR)",
|
||||
);
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch";
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = "LimelightTests/LimelightTests-Info.plist";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUNDLE_LOADER)";
|
||||
WRAPPER_EXTENSION = xctest;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FB290D2519B2C406004C83CF /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Limelight.app/Limelight";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
"$(DEVELOPER_FRAMEWORKS_DIR)",
|
||||
);
|
||||
GCC_PRECOMPILE_PREFIX_HEADER = YES;
|
||||
GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch";
|
||||
INFOPLIST_FILE = "LimelightTests/LimelightTests-Info.plist";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUNDLE_LOADER)";
|
||||
WRAPPER_EXTENSION = xctest;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "Limelight" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FB290D1E19B2C406004C83CF /* Debug */,
|
||||
FB290D1F19B2C406004C83CF /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
FB290D2019B2C406004C83CF /* Build configuration list for PBXNativeTarget "Limelight" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FB290D2119B2C406004C83CF /* Debug */,
|
||||
FB290D2219B2C406004C83CF /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
FB290D2319B2C406004C83CF /* Build configuration list for PBXNativeTarget "LimelightTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FB290D2419B2C406004C83CF /* Debug */,
|
||||
FB290D2519B2C406004C83CF /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
FB290D0619B2C406004C83CF /* Limelight.xcdatamodel */,
|
||||
);
|
||||
currentVersion = FB290D0619B2C406004C83CF /* Limelight.xcdatamodel */;
|
||||
path = Limelight.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
/* End XCVersionGroup section */
|
||||
};
|
||||
rootObject = FB290CE619B2C406004C83CF /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:Limelight.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
|
||||
<false/>
|
||||
<key>IDESourceControlProjectIdentifier</key>
|
||||
<string>054A62F3-1197-45FB-A607-EA7922396E24</string>
|
||||
<key>IDESourceControlProjectName</key>
|
||||
<string>Limelight</string>
|
||||
<key>IDESourceControlProjectOriginsDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>github.com:limelight-stream/limelight-common-c.git</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectPath</key>
|
||||
<string>Limelight.xcodeproj</string>
|
||||
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
|
||||
<dict>
|
||||
<key>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</key>
|
||||
<string>../..</string>
|
||||
<key>EB99D631D5149240E91F8D168F0F298D7A5BAA89</key>
|
||||
<string>../../limelight-common-c</string>
|
||||
</dict>
|
||||
<key>IDESourceControlProjectURL</key>
|
||||
<string>github.com:limelight-stream/limelight-ios.git</string>
|
||||
<key>IDESourceControlProjectVersion</key>
|
||||
<integer>111</integer>
|
||||
<key>IDESourceControlProjectWCCIdentifier</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlProjectWCConfigurations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>EB99D631D5149240E91F8D168F0F298D7A5BAA89</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-common-c</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
|
||||
<string>public.vcs.git</string>
|
||||
<key>IDESourceControlWCCIdentifierKey</key>
|
||||
<string>A4B0849259F9566B0CBA84A6D3D4A0F895C03123</string>
|
||||
<key>IDESourceControlWCCName</key>
|
||||
<string>limelight-ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// AppDelegate.h
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/17/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
||||
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
|
||||
- (void)saveContext;
|
||||
- (NSURL *)applicationDocumentsDirectory;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// AppDelegate.m
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/17/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
|
||||
@synthesize managedObjectContext = _managedObjectContext;
|
||||
@synthesize managedObjectModel = _managedObjectModel;
|
||||
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
|
||||
|
||||
static NSOperationQueue* mainQueue;
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (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.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
- (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.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application
|
||||
{
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(UIApplication *)application
|
||||
{
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application
|
||||
{
|
||||
// Saves changes in the application's managed object context before the application terminates.
|
||||
[self saveContext];
|
||||
}
|
||||
|
||||
- (void)saveContext
|
||||
{
|
||||
NSError *error = nil;
|
||||
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
|
||||
if (managedObjectContext != nil) {
|
||||
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Core Data stack
|
||||
|
||||
// Returns the managed object context for the application.
|
||||
// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
|
||||
- (NSManagedObjectContext *)managedObjectContext
|
||||
{
|
||||
if (_managedObjectContext != nil) {
|
||||
return _managedObjectContext;
|
||||
}
|
||||
|
||||
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
|
||||
if (coordinator != nil) {
|
||||
_managedObjectContext = [[NSManagedObjectContext alloc] init];
|
||||
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
|
||||
}
|
||||
return _managedObjectContext;
|
||||
}
|
||||
|
||||
// Returns the managed object model for the application.
|
||||
// If the model doesn't already exist, it is created from the application's model.
|
||||
- (NSManagedObjectModel *)managedObjectModel
|
||||
{
|
||||
if (_managedObjectModel != nil) {
|
||||
return _managedObjectModel;
|
||||
}
|
||||
_managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];
|
||||
return _managedObjectModel;
|
||||
}
|
||||
|
||||
// Returns the persistent store coordinator for the application.
|
||||
// If the coordinator doesn't already exist, it is created and the application's store added to it.
|
||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
|
||||
{
|
||||
if (_persistentStoreCoordinator != nil) {
|
||||
return _persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Limelight_iOS.sqlite"];
|
||||
|
||||
NSError *error = nil;
|
||||
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
||||
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
|
||||
/*
|
||||
Replace this implementation with code to handle the error appropriately.
|
||||
|
||||
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
||||
Typical reasons for an error here include:
|
||||
* The persistent store is not accessible;
|
||||
* The schema for the persistent store is incompatible with current managed object model.
|
||||
Check the error message to determine what the actual problem was.
|
||||
|
||||
|
||||
If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
|
||||
|
||||
If you encounter schema incompatibility errors during development, you can reduce their frequency by:
|
||||
* Simply deleting the existing store:
|
||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
|
||||
|
||||
* Performing automatic lightweight migration by passing the following dictionary as the options parameter:
|
||||
@{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}
|
||||
|
||||
Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
|
||||
|
||||
*/
|
||||
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
|
||||
abort();
|
||||
}
|
||||
|
||||
return _persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
#pragma mark - Application's Documents directory
|
||||
|
||||
// Returns the URL to the application's Documents directory.
|
||||
- (NSURL *)applicationDocumentsDirectory
|
||||
{
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// CryptoManager.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/14/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface CryptoManager : NSObject
|
||||
|
||||
+ (void) generateKeyPairUsingSSl;
|
||||
+ (NSString*) getUniqueID;
|
||||
+ (NSData*) readCertFromFile;
|
||||
+ (NSData*) readKeyFromFile;
|
||||
+ (NSData*) readP12FromFile;
|
||||
+ (NSData*) getSignatureFromCert:(NSData*)cert;
|
||||
|
||||
- (NSData*) createAESKeyFromSalt:(NSData*)saltedPIN;
|
||||
- (NSData*) SHA1HashData:(NSData*)data;
|
||||
- (NSData*) aesEncrypt:(NSData*)data withKey:(NSData*)key;
|
||||
- (NSData*) aesDecrypt:(NSData*)data withKey:(NSData*)key;
|
||||
- (bool) verifySignature:(NSData *)data withSignature:(NSData*)signature andCert:(NSData*)cert;
|
||||
- (NSData*) signData:(NSData*)data withKey:(NSData*)key;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// CryptoManager.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/14/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CryptoManager.h"
|
||||
#import "mkcert.h"
|
||||
#import <AdSupport/ASIdentifierManager.h>
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
@implementation CryptoManager
|
||||
static const int SHA1_DIGEST_LENGTH = 20;
|
||||
static NSData* key = nil;
|
||||
static NSData* cert = nil;
|
||||
static NSData* p12 = nil;
|
||||
|
||||
- (NSData*) createAESKeyFromSalt:(NSData*)saltedPIN {
|
||||
return [[self SHA1HashData:saltedPIN] subdataWithRange:NSMakeRange(0, 16)];
|
||||
}
|
||||
|
||||
- (NSData*) SHA1HashData:(NSData*)data {
|
||||
unsigned char sha1[SHA1_DIGEST_LENGTH];
|
||||
SHA1([data bytes], [data length], sha1);
|
||||
NSData* bytes = [NSData dataWithBytes:sha1 length:20];
|
||||
return bytes;
|
||||
}
|
||||
|
||||
- (NSData*) aesEncrypt:(NSData*)data withKey:(NSData*)key {
|
||||
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]);
|
||||
|
||||
// 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 {
|
||||
AES_KEY aesKey;
|
||||
AES_set_decrypt_key([key bytes], 128, &aesKey);
|
||||
unsigned char* buffer = malloc([data length]);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
- (bool) verifySignature:(NSData *)data withSignature:(NSData*)signature andCert:(NSData*)cert {
|
||||
const char* buffer = [cert bytes];
|
||||
X509* x509;
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
BIO_puts(bio, buffer);
|
||||
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
|
||||
BIO_free(bio);
|
||||
|
||||
if (!x509) {
|
||||
fprintf(stderr, "unable to parse certificate in memory\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EVP_PKEY* pubKey = X509_get_pubkey(x509);
|
||||
EVP_MD_CTX *mdctx = NULL;
|
||||
mdctx = EVP_MD_CTX_create();
|
||||
EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pubKey);
|
||||
EVP_DigestVerifyUpdate(mdctx, [data bytes], [data length]);
|
||||
int result = EVP_DigestVerifyFinal(mdctx, (unsigned char*)[signature bytes], [signature length]);
|
||||
|
||||
X509_free(x509);
|
||||
EVP_PKEY_free(pubKey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
- (NSData *)signData:(NSData *)data withKey:(NSData *)key {
|
||||
const char* buffer = [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);
|
||||
|
||||
BIO_free(bio);
|
||||
|
||||
if (!pkey) {
|
||||
fprintf(stderr, "unable to parse private key in memory\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
EVP_MD_CTX *mdctx = NULL;
|
||||
mdctx = EVP_MD_CTX_create();
|
||||
EVP_DigestSignInit(mdctx, NULL, EVP_sha256(), NULL, pkey);
|
||||
EVP_DigestSignUpdate(mdctx, [data bytes], [data length]);
|
||||
size_t slen;
|
||||
EVP_DigestSignFinal(mdctx, NULL, &slen);
|
||||
unsigned char* signature = malloc(slen);
|
||||
int result = EVP_DigestSignFinal(mdctx, signature, &slen);
|
||||
|
||||
EVP_PKEY_free(pkey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
|
||||
if (result <= 0) {
|
||||
free(signature);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSData* signedData = [NSData dataWithBytes:signature length:slen];
|
||||
free(signature);
|
||||
|
||||
return signedData;
|
||||
}
|
||||
|
||||
// TODO: these three methods are almost identical, fix the copy-pasta
|
||||
+ (NSData*) readCertFromFile {
|
||||
if (cert == nil) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
const char* buffer = [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) {
|
||||
fprintf(stderr, "unable to parse certificate in memory\n");
|
||||
return NULL;
|
||||
}
|
||||
return [NSData dataWithBytes:x509->signature->data length:x509->signature->length];
|
||||
}
|
||||
|
||||
+ (void) generateKeyPairUsingSSl {
|
||||
if (![CryptoManager keyPairExists]) {
|
||||
|
||||
NSLog(@"Generating Certificate... ");
|
||||
CertKeyPair certKeyPair = generateCertKeyPair();
|
||||
|
||||
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"];
|
||||
|
||||
//NSLog(@"Writing cert and key to: \n%@\n%@", certFile, keyPairFile);
|
||||
saveCertKeyPair([certFile UTF8String], [p12File UTF8String], [keyPairFile UTF8String], certKeyPair);
|
||||
freeCertKeyPair(certKeyPair);
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSString*) getUniqueID {
|
||||
// generate a UUID
|
||||
NSUUID* uuid = [ASIdentifierManager sharedManager].advertisingIdentifier;
|
||||
NSString* idString = [NSString stringWithString:[uuid UUIDString]];
|
||||
|
||||
// we need a 16byte hex-string so we take the last 17 characters
|
||||
// and remove the '-' to get a 16 character string
|
||||
NSMutableString* uniqueId = [NSMutableString stringWithString:[idString substringFromIndex:19]];
|
||||
[uniqueId deleteCharactersInRange:NSMakeRange(4, 1)];
|
||||
|
||||
//NSLog(@"Unique ID: %@", uniqueId);
|
||||
return [NSString stringWithString:uniqueId];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,169 @@
|
||||
|
||||
#include "mkcert.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/conf.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
|
||||
#ifndef OPENSSL_NO_ENGINE
|
||||
#include <openssl/engine.h>
|
||||
#endif
|
||||
|
||||
static const int NUM_BITS = 2048;
|
||||
static const int SERIAL = 0;
|
||||
static const int NUM_YEARS = 10;
|
||||
|
||||
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
|
||||
int add_ext(X509 *cert, int nid, char *value);
|
||||
|
||||
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);
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
void freeCertKeyPair(struct CertKeyPair certKeyPair) {
|
||||
X509_free(certKeyPair.x509);
|
||||
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) {
|
||||
abort();
|
||||
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)) {
|
||||
abort();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// mkcert.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/16/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Limelight_mkcert_h
|
||||
#define Limelight_mkcert_h
|
||||
|
||||
#include <openssl/x509v3.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
|
||||
typedef struct CertKeyPair {
|
||||
X509 *x509;
|
||||
EVP_PKEY *pkey;
|
||||
PKCS12 *p12;
|
||||
} CertKeyPair;
|
||||
|
||||
struct CertKeyPair generateCertKeyPair();
|
||||
void freeCertKeyPair(CertKeyPair);
|
||||
void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "app_icon_58x58_2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "app_icon_80x80_2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "app_icon_120x120_2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "app_icon_180x180_3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "app_icon_29x29_1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "app_icon_58x58_2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "app_icon_40x40_1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "app_icon_80x80_2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "app_icon_76x76_1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "app_icon_152x152_2x.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"orientation" : "portrait",
|
||||
"idiom" : "iphone",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "7.0",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"orientation" : "portrait",
|
||||
"idiom" : "iphone",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "7.0",
|
||||
"subtype" : "retina4",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"orientation" : "portrait",
|
||||
"idiom" : "ipad",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "7.0",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"orientation" : "landscape",
|
||||
"idiom" : "ipad",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "7.0",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"orientation" : "portrait",
|
||||
"idiom" : "ipad",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "7.0",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"orientation" : "landscape",
|
||||
"idiom" : "ipad",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "7.0",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "store_icon_1024x1024.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 627 KiB |
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// ControllerSupport.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface ControllerSupport : NSObject
|
||||
|
||||
-(id) init;
|
||||
|
||||
-(void) cleanup;
|
||||
|
||||
@property (nonatomic, strong) id connectObserver;
|
||||
@property (nonatomic, strong) id disconnectObserver;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// ControllerSupport.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#include "Limelight.h"
|
||||
|
||||
@import GameController;
|
||||
|
||||
@implementation ControllerSupport
|
||||
|
||||
// UPDATE_BUTTON(flag, pressed)
|
||||
#define UPDATE_BUTTON(x, y) (buttonFlags = \
|
||||
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
|
||||
|
||||
static NSLock *controllerStreamLock;
|
||||
|
||||
+(void) registerControllerCallbacks
|
||||
{
|
||||
for (int i = 0; i < [[GCController controllers] count]; i++) {
|
||||
GCController *controller = [GCController controllers][i];
|
||||
|
||||
if (controller != NULL) {
|
||||
printf("Controller connected!\n");
|
||||
controller.controllerPausedHandler = ^(GCController *controller) {
|
||||
// We call LiSendControllerEvent while holding a lock to prevent
|
||||
// multiple simultaneous calls since this function isn't thread safe.
|
||||
[controllerStreamLock lock];
|
||||
LiSendControllerEvent(PLAY_FLAG, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
// Pause for 100 ms
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendControllerEvent(0, 0, 0, 0, 0, 0, 0);
|
||||
[controllerStreamLock unlock];
|
||||
};
|
||||
|
||||
if (controller.extendedGamepad != NULL) {
|
||||
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
|
||||
short buttonFlags = 0;
|
||||
short leftStickX, leftStickY;
|
||||
short rightStickX, rightStickY;
|
||||
char leftTrigger, rightTrigger;
|
||||
|
||||
UPDATE_BUTTON(A_FLAG, gamepad.buttonA.pressed);
|
||||
UPDATE_BUTTON(B_FLAG, gamepad.buttonB.pressed);
|
||||
UPDATE_BUTTON(X_FLAG, gamepad.buttonX.pressed);
|
||||
UPDATE_BUTTON(Y_FLAG, gamepad.buttonY.pressed);
|
||||
|
||||
UPDATE_BUTTON(UP_FLAG, gamepad.dpad.up.pressed);
|
||||
UPDATE_BUTTON(DOWN_FLAG, gamepad.dpad.down.pressed);
|
||||
UPDATE_BUTTON(LEFT_FLAG, gamepad.dpad.left.pressed);
|
||||
UPDATE_BUTTON(RIGHT_FLAG, gamepad.dpad.right.pressed);
|
||||
|
||||
UPDATE_BUTTON(LB_FLAG, gamepad.leftShoulder.pressed);
|
||||
UPDATE_BUTTON(RB_FLAG, gamepad.rightShoulder.pressed);
|
||||
|
||||
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
|
||||
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
|
||||
|
||||
rightStickX = gamepad.rightThumbstick.xAxis.value * 0x7FFE;
|
||||
rightStickY = gamepad.rightThumbstick.yAxis.value * 0x7FFE;
|
||||
|
||||
leftTrigger = gamepad.leftTrigger.value * 0xFF;
|
||||
rightTrigger = gamepad.rightTrigger.value * 0xFF;
|
||||
|
||||
// We call LiSendControllerEvent while holding a lock to prevent
|
||||
// multiple simultaneous calls since this function isn't thread safe.
|
||||
[controllerStreamLock lock];
|
||||
LiSendControllerEvent(buttonFlags, leftTrigger, rightTrigger,
|
||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
[controllerStreamLock unlock];
|
||||
};
|
||||
}
|
||||
else if (controller.gamepad != NULL) {
|
||||
controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, GCControllerElement *element) {
|
||||
short buttonFlags = 0;
|
||||
|
||||
UPDATE_BUTTON(A_FLAG, gamepad.buttonA.pressed);
|
||||
UPDATE_BUTTON(B_FLAG, gamepad.buttonB.pressed);
|
||||
UPDATE_BUTTON(X_FLAG, gamepad.buttonX.pressed);
|
||||
UPDATE_BUTTON(Y_FLAG, gamepad.buttonY.pressed);
|
||||
|
||||
UPDATE_BUTTON(UP_FLAG, gamepad.dpad.up.pressed);
|
||||
UPDATE_BUTTON(DOWN_FLAG, gamepad.dpad.down.pressed);
|
||||
UPDATE_BUTTON(LEFT_FLAG, gamepad.dpad.left.pressed);
|
||||
UPDATE_BUTTON(RIGHT_FLAG, gamepad.dpad.right.pressed);
|
||||
|
||||
UPDATE_BUTTON(LB_FLAG, gamepad.leftShoulder.pressed);
|
||||
UPDATE_BUTTON(RB_FLAG, gamepad.rightShoulder.pressed);
|
||||
|
||||
// We call LiSendControllerEvent while holding a lock to prevent
|
||||
// multiple simultaneous calls since this function isn't thread safe.
|
||||
[controllerStreamLock lock];
|
||||
LiSendControllerEvent(buttonFlags, 0, 0, 0, 0, 0, 0);
|
||||
[controllerStreamLock unlock];
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-(id) init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (controllerStreamLock == NULL) {
|
||||
controllerStreamLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
[ControllerSupport registerControllerCallbacks];
|
||||
}];
|
||||
self.disconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
printf("Controller disconnected!\n");
|
||||
}];
|
||||
|
||||
[ControllerSupport registerControllerCallbacks];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void) cleanup
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// StreamView.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/19/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface StreamView : UIView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// StreamView.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/19/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamView.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation StreamView {
|
||||
CGPoint touchLocation;
|
||||
BOOL touchMoved;
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
touchLocation = [touch locationInView:self];
|
||||
touchMoved = false;
|
||||
|
||||
printf("Touch down\n");
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
CGPoint currentLocation = [touch locationInView:self];
|
||||
|
||||
if (touchLocation.x != currentLocation.x ||
|
||||
touchLocation.y != currentLocation.y)
|
||||
{
|
||||
LiSendMouseMoveEvent(currentLocation.x - touchLocation.x,
|
||||
currentLocation.y - touchLocation.y );
|
||||
|
||||
touchMoved = true;
|
||||
touchLocation = currentLocation;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
printf("Touch up\n");
|
||||
|
||||
if (!touchMoved) {
|
||||
printf("Sending left mouse button press\n");
|
||||
|
||||
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)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.limelight-stream.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>7</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Launch Screen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>MainFrame-iPhone</string>
|
||||
<key>UIMainStoryboardFile~ipad</key>
|
||||
<string>MainFrame-iPad</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<true/>
|
||||
<key>UIStatusBarHidden~ipad</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Prefix header
|
||||
//
|
||||
// The contents of this file are implicitly included at the beginning of every source file.
|
||||
//
|
||||
|
||||
#import <Availability.h>
|
||||
|
||||
#ifndef __IPHONE_3_0
|
||||
#warning "This project uses features only available in iOS SDK 3.0 and later."
|
||||
#endif
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Limelight.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model name="Test1.xcdatamodel" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<elements/>
|
||||
</model>
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// HttpManager.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/16/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpManager : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate>
|
||||
|
||||
+ (NSString*) getStringFromXML:(NSData*)xml tag:(NSString*)tag;
|
||||
+ (NSString*) getStatusStringFromXML:(NSData*)xml;
|
||||
|
||||
- (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;
|
||||
- (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;
|
||||
- (NSData*) executeRequestSynchronously:(NSURLRequest*)request;
|
||||
@end
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
//
|
||||
// HttpManager.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/16/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HttpManager.h"
|
||||
#import "CryptoManager.h"
|
||||
|
||||
#include <libxml2/libxml/xmlreader.h>
|
||||
#include <string.h>
|
||||
|
||||
@implementation HttpManager {
|
||||
NSString* _baseURL;
|
||||
NSString* _host;
|
||||
NSString* _uniqueId;
|
||||
NSString* _deviceName;
|
||||
NSData* _cert;
|
||||
NSMutableData* _respData;
|
||||
NSData* _requestResp;
|
||||
dispatch_semaphore_t _requestLock;
|
||||
}
|
||||
|
||||
static const NSString* PORT = @"47984";
|
||||
|
||||
+ (NSString*) getStatusStringFromXML:(NSData*)xml {
|
||||
xmlDocPtr docPtr = xmlParseMemory([xml bytes], (int)[xml length]);
|
||||
|
||||
if (docPtr == NULL) {
|
||||
NSLog(@"ERROR: An error occured trying to parse xml.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSString* string;
|
||||
xmlNodePtr rootNode = xmlDocGetRootElement(docPtr);
|
||||
|
||||
string = [HttpManager getStatusMessage: rootNode];
|
||||
xmlFree(rootNode);
|
||||
xmlFree(docPtr);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
+ (NSString*) getStringFromXML:(NSData*)xml tag:(NSString*)tag {
|
||||
xmlDocPtr docPtr = xmlParseMemory([xml bytes], (int)[xml length]);
|
||||
|
||||
if (docPtr == NULL) {
|
||||
NSLog(@"ERROR: An error occured trying to parse xml.");
|
||||
return NULL;
|
||||
}
|
||||
NSString* value;
|
||||
xmlNodePtr node;
|
||||
xmlNodePtr rootNode = node = xmlDocGetRootElement(docPtr);
|
||||
|
||||
// Check root status_code
|
||||
if (![HttpManager verifyStatus: rootNode]) {
|
||||
NSLog(@"ERROR: Request returned with failure status");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Skip the root node
|
||||
node = node->children;
|
||||
|
||||
while (node != NULL) {
|
||||
//NSLog(@"node: %s", node->name);
|
||||
if (!xmlStrcmp(node->name, (const xmlChar*)[tag UTF8String])) {
|
||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, node->xmlChildrenNode, 1);
|
||||
value = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||
xmlFree(nodeVal);
|
||||
}
|
||||
node = node->next;
|
||||
}
|
||||
//NSLog(@"xmlValue: %@", value);
|
||||
xmlFree(rootNode);
|
||||
xmlFree(docPtr);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
+ (NSString*) getStatusMessage:(xmlNodePtr)docRoot {
|
||||
xmlChar* statusMsgXml = xmlGetProp(docRoot, (const xmlChar*)"status_message");
|
||||
NSString* statusMsg = [NSString stringWithUTF8String:(const char*)statusMsgXml];
|
||||
xmlFree(statusMsgXml);
|
||||
return statusMsg;
|
||||
}
|
||||
|
||||
+ (bool) verifyStatus:(xmlNodePtr)docRoot {
|
||||
xmlChar* statusStr = xmlGetProp(docRoot, (const xmlChar*)"status_code");
|
||||
NSLog(@"status: %s", statusStr);
|
||||
int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
|
||||
xmlFree(statusStr);
|
||||
return status == 200;
|
||||
}
|
||||
|
||||
+ (NSData*) fixXmlVersion:(NSData*) xmlData {
|
||||
NSString* xmlString = [[[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [xmlData length])];
|
||||
//NSLog(@"xmlString: %@", xmlString);
|
||||
return [NSData dataWithBytes:[xmlString UTF8String] length:[xmlString length]];
|
||||
}
|
||||
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert {
|
||||
self = [super init];
|
||||
_host = host;
|
||||
_uniqueId = uniqueId;
|
||||
_deviceName = deviceName;
|
||||
_cert = cert;
|
||||
_baseURL = [[NSString stringWithFormat:@"https://%@:%@", host, PORT]
|
||||
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
_requestLock = dispatch_semaphore_create(0);
|
||||
_respData = [[NSMutableData alloc] init];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSData*) executeRequestSynchronously:(NSURLRequest*)request {
|
||||
NSLog(@"Making Request: %@", request);
|
||||
[_respData setLength:0];
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[NSURLConnection connectionWithRequest:request delegate:self];
|
||||
});
|
||||
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
|
||||
return _requestResp;
|
||||
}
|
||||
|
||||
- (void) executeRequest:(NSURLRequest*)request {
|
||||
[NSURLConnection connectionWithRequest:request delegate:self];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) createRequestFromString:(NSString*) urlString enableTimeout:(BOOL)normalTimeout {
|
||||
NSString* escapedUrl = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
||||
NSURL* url = [[NSURL alloc] initWithString:escapedUrl];
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||
if (normalTimeout) {
|
||||
// Timeout the request after 5 seconds
|
||||
[request setTimeoutInterval:5];
|
||||
}
|
||||
else {
|
||||
// Timeout the request after 60 seconds
|
||||
[request setTimeoutInterval:60];
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newPairRequest:(NSData*)salt {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=getservercert&salt=%@&clientcert=%@",
|
||||
_baseURL, _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 enableTimeout:FALSE];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newUnpairRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientchallenge=%@",
|
||||
_baseURL, _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=%@",
|
||||
_baseURL, _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=%@", _baseURL, _uniqueId, _deviceName, clientPairSecret];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newPairChallenge {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseURL, _uniqueId, _deviceName];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newAppListRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newServerInfoRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
}
|
||||
|
||||
- (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", _baseURL, _uniqueId, appId, width, height, refreshRate, rikey, rikeyid];
|
||||
// This blocks while the app is launching
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d", _baseURL, _uniqueId, riKey, riKeyId];
|
||||
// This blocks while the app is resuming
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
}
|
||||
|
||||
- (NSString*) bytesToHex:(NSData*)data {
|
||||
const unsigned char* bytes = [data bytes];
|
||||
NSMutableString *hex = [[NSMutableString alloc] init];
|
||||
for (int i = 0; i < [data length]; i++) {
|
||||
[hex appendFormat:@"%02X" , bytes[i]];
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
|
||||
NSLog(@"Received response: %@", response);
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
|
||||
NSLog(@"Received data: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
|
||||
[_respData appendData:data];
|
||||
}
|
||||
|
||||
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
|
||||
_requestResp = [HttpManager fixXmlVersion:_respData];
|
||||
dispatch_semaphore_signal(_requestLock);
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
|
||||
SecIdentityRef identity = [self getClientCertificate];
|
||||
NSArray *certArray = [self getCertificate:identity];
|
||||
|
||||
NSURLCredential *newCredential = [NSURLCredential credentialWithIdentity:identity certificates:certArray persistence:NSURLCredentialPersistencePermanent];
|
||||
|
||||
[challenge.sender useCredential:newCredential forAuthenticationChallenge:challenge];
|
||||
}
|
||||
|
||||
// Returns an array containing the certificate
|
||||
- (NSArray*)getCertificate:(SecIdentityRef) identity {
|
||||
SecCertificateRef certificate = nil;
|
||||
|
||||
SecIdentityCopyCertificate(identity, &certificate);
|
||||
|
||||
return [[NSArray alloc] initWithObjects:(__bridge id)certificate, nil];
|
||||
}
|
||||
|
||||
// Returns the identity
|
||||
- (SecIdentityRef)getClientCertificate {
|
||||
SecIdentityRef identityApp = nil;
|
||||
CFDataRef p12Data = (__bridge CFDataRef)[CryptoManager readP12FromFile];
|
||||
|
||||
CFStringRef password = CFSTR("limelight");
|
||||
const void *keys[] = { kSecImportExportPassphrase };
|
||||
const void *values[] = { password };
|
||||
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
|
||||
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
|
||||
OSStatus securityError = SecPKCS12Import(p12Data, options, &items);
|
||||
|
||||
if (securityError == errSecSuccess) {
|
||||
//NSLog(@"Success opening p12 certificate. Items: %ld", CFArrayGetCount(items));
|
||||
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
|
||||
identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
|
||||
} else {
|
||||
NSLog(@"Error opening Certificate.");
|
||||
}
|
||||
|
||||
CFRelease(options);
|
||||
CFRelease(password);
|
||||
|
||||
return identityApp;
|
||||
}
|
||||
|
||||
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
|
||||
NSLog(@"connection error: %@", error);
|
||||
dispatch_semaphore_signal(_requestLock);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// MDNSManager.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/14/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol MDNSCallback <NSObject>
|
||||
|
||||
- (void) updateHosts:(NSArray*)hosts;
|
||||
|
||||
@end
|
||||
|
||||
@interface MDNSManager : NSObject <NSNetServiceBrowserDelegate, NSNetServiceDelegate>
|
||||
|
||||
@property id<MDNSCallback> callback;
|
||||
|
||||
- (id) initWithCallback:(id<MDNSCallback>) callback;
|
||||
- (void) searchForHosts;
|
||||
- (void) stopSearching;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// MDNSManager.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/14/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MDNSManager.h"
|
||||
#import "Computer.h"
|
||||
|
||||
@implementation MDNSManager {
|
||||
NSNetServiceBrowser* mDNSBrowser;
|
||||
NSMutableArray* domains;
|
||||
NSMutableArray* services;
|
||||
BOOL scanActive;
|
||||
}
|
||||
|
||||
static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
|
||||
- (id) initWithCallback:(id<MDNSCallback>)callback {
|
||||
self = [super init];
|
||||
|
||||
self.callback = callback;
|
||||
|
||||
mDNSBrowser = [[NSNetServiceBrowser alloc] init];
|
||||
[mDNSBrowser setDelegate:self];
|
||||
|
||||
domains = [[NSMutableArray alloc] init];
|
||||
services = [[NSMutableArray alloc] init];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) searchForHosts {
|
||||
NSLog(@"Starting mDNS discovery");
|
||||
scanActive = TRUE;
|
||||
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
|
||||
}
|
||||
|
||||
- (void) stopSearching {
|
||||
NSLog(@"Stopping mDNS discovery");
|
||||
scanActive = FALSE;
|
||||
[mDNSBrowser stop];
|
||||
}
|
||||
|
||||
- (NSArray*) getFoundHosts {
|
||||
NSMutableArray* hosts = [[NSMutableArray alloc] init];
|
||||
for (NSNetService* service in services) {
|
||||
if (service.hostName != nil) {
|
||||
[hosts addObject:[[Computer alloc] initWithHost:service]];
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
}
|
||||
|
||||
- (void)netServiceDidResolveAddress:(NSNetService *)service {
|
||||
NSLog(@"Resolved address: %@ -> %@", service, service.hostName);
|
||||
[self.callback updateHosts:[self getFoundHosts]];
|
||||
}
|
||||
|
||||
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
|
||||
NSLog(@"Did not resolve address for: %@\n%@", sender, [errorDict description]);
|
||||
|
||||
// Schedule a retry in 2 seconds
|
||||
[NSTimer scheduledTimerWithTimeInterval:2.0
|
||||
target:self
|
||||
selector:@selector(retryResolveTimerCallback:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
||||
NSLog(@"Found service: %@", aNetService);
|
||||
[aNetService setDelegate:self];
|
||||
[aNetService resolveWithTimeout:5];
|
||||
|
||||
[services removeObject:aNetService];
|
||||
[services addObject:aNetService];
|
||||
}
|
||||
|
||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
||||
NSLog(@"Removing service: %@", aNetService);
|
||||
[services removeObject:aNetService];
|
||||
}
|
||||
|
||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict {
|
||||
NSLog(@"Did not perform search");
|
||||
NSLog(@"%@", [errorDict description]);
|
||||
|
||||
// Schedule a retry in 2 seconds
|
||||
[NSTimer scheduledTimerWithTimeInterval:2.0
|
||||
target:self
|
||||
selector:@selector(retrySearchTimerCallback:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
- (void)retrySearchTimerCallback:(NSTimer *)timer {
|
||||
// Check if we've been stopped since this was queued
|
||||
if (!scanActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"Retrying mDNS search");
|
||||
[mDNSBrowser stop];
|
||||
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
|
||||
}
|
||||
|
||||
- (void)retryResolveTimerCallback:(NSTimer *)timer {
|
||||
// Check if we've been stopped since this was queued
|
||||
if (!scanActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"Retrying mDNS resolution");
|
||||
for (NSNetService* service in services) {
|
||||
if (service.hostName == nil) {
|
||||
[service setDelegate:self];
|
||||
[service resolveWithTimeout:5];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// PairManager.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/19/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpManager.h"
|
||||
|
||||
@protocol PairCallback <NSObject>
|
||||
|
||||
- (void) showPIN:(NSString*)PIN;
|
||||
- (void) pairSuccessful;
|
||||
- (void) pairFailed:(NSString*)message;
|
||||
|
||||
@end
|
||||
|
||||
@interface PairManager : NSOperation
|
||||
- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id<PairCallback>)callback;
|
||||
- (NSString*) generatePIN;
|
||||
- (NSData*) saltPIN:(NSString*)PIN;
|
||||
- (void) initiatePair;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// PairManager.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/19/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "PairManager.h"
|
||||
#import "CryptoManager.h"
|
||||
#import "Utils.h"
|
||||
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
@implementation PairManager {
|
||||
HttpManager* _httpManager;
|
||||
NSData* _cert;
|
||||
id<PairCallback> _callback;
|
||||
}
|
||||
|
||||
- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id<PairCallback>)callback {
|
||||
self = [super init];
|
||||
_httpManager = httpManager;
|
||||
_cert = cert;
|
||||
_callback = callback;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) main {
|
||||
NSData* serverInfo = [_httpManager executeRequestSynchronously:[_httpManager newServerInfoRequest]];
|
||||
if (serverInfo == NULL) {
|
||||
[_callback pairFailed:@"Unable to connect to PC"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (![[HttpManager getStringFromXML:serverInfo tag:@"currentgame"] isEqual:@"0"]) {
|
||||
[_callback pairFailed:@"You must stop streaming before attempting to pair."];
|
||||
}
|
||||
else if (![[HttpManager getStringFromXML:serverInfo tag:@"PairStatus"] isEqual:@"1"]) {
|
||||
[self initiatePair];
|
||||
} else {
|
||||
[_callback pairFailed:@"This device is already paired."];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) initiatePair {
|
||||
NSString* PIN = [self generatePIN];
|
||||
NSData* salt = [self saltPIN:PIN];
|
||||
NSLog(@"PIN: %@, saltedPIN: %@", PIN, salt);
|
||||
[_callback showPIN:PIN];
|
||||
|
||||
NSData* pairResp = [_httpManager executeRequestSynchronously:[_httpManager newPairRequest:salt]];
|
||||
NSString* pairedString;
|
||||
pairedString = [HttpManager getStringFromXML:pairResp tag:@"paired"];
|
||||
if (pairedString == NULL || ![pairedString isEqualToString:@"1"]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Pairing was declined by the target."];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* plainCert = [HttpManager getStringFromXML:pairResp tag:@"plaincert"];
|
||||
|
||||
CryptoManager* cryptoMan = [[CryptoManager alloc] init];
|
||||
NSData* aesKey = [cryptoMan createAESKeyFromSalt:salt];
|
||||
|
||||
NSData* randomChallenge = [Utils randomBytes:16];
|
||||
NSData* encryptedChallenge = [cryptoMan aesEncrypt:randomChallenge withKey:aesKey];
|
||||
|
||||
NSData* challengeResp = [_httpManager executeRequestSynchronously:[_httpManager newChallengeRequest:encryptedChallenge]];
|
||||
pairedString = [HttpManager getStringFromXML:challengeResp tag:@"paired"];
|
||||
if (pairedString == NULL || ![pairedString isEqualToString:@"1"]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Pairing stage #2 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* encServerChallengeResp = [Utils hexToBytes:[HttpManager getStringFromXML:challengeResp tag:@"challengeresponse"]];
|
||||
NSData* decServerChallengeResp = [cryptoMan aesDecrypt:encServerChallengeResp withKey:aesKey];
|
||||
|
||||
NSData* serverResponse = [decServerChallengeResp subdataWithRange:NSMakeRange(0, 20)];
|
||||
NSData* serverChallenge = [decServerChallengeResp subdataWithRange:NSMakeRange(20, 16)];
|
||||
|
||||
NSData* clientSecret = [Utils randomBytes:16];
|
||||
NSData* challengeRespHash = [cryptoMan SHA1HashData:[self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_cert]] with:clientSecret]];
|
||||
NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:challengeRespHash withKey:aesKey];
|
||||
|
||||
NSData* secretResp = [_httpManager executeRequestSynchronously:[_httpManager newChallengeRespRequest:challengeRespEncrypted]];
|
||||
pairedString = [HttpManager getStringFromXML:secretResp tag:@"paired"];
|
||||
if (pairedString == NULL || ![pairedString isEqualToString:@"1"]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Pairing stage #3 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* serverSecretResp = [Utils hexToBytes:[HttpManager getStringFromXML:secretResp tag:@"pairingsecret"]];
|
||||
NSData* serverSecret = [serverSecretResp subdataWithRange:NSMakeRange(0, 16)];
|
||||
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, 256)];
|
||||
|
||||
if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Server certificate invalid"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* serverChallengeRespHash = [cryptoMan SHA1HashData:[self concatData:[self concatData:randomChallenge with:[CryptoManager getSignatureFromCert:[Utils hexToBytes:plainCert]]] with:serverSecret]];
|
||||
if (![serverChallengeRespHash isEqual:serverResponse]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Incorrect PIN"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* clientPairingSecret = [self concatData:clientSecret with:[cryptoMan signData:clientSecret withKey:[CryptoManager readKeyFromFile]]];
|
||||
NSData* clientSecretResp = [_httpManager executeRequestSynchronously:[_httpManager newClientSecretRespRequest:[Utils bytesToHex:clientPairingSecret]]];
|
||||
pairedString = [HttpManager getStringFromXML:clientSecretResp tag:@"paired"];
|
||||
if (pairedString == NULL || ![pairedString isEqualToString:@"1"]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Pairing stage #4 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* clientPairChallenge = [_httpManager executeRequestSynchronously:[_httpManager newPairChallenge]];
|
||||
pairedString = [HttpManager getStringFromXML:clientPairChallenge tag:@"paired"];
|
||||
if (pairedString == NULL || ![pairedString isEqualToString:@"1"]) {
|
||||
[_httpManager executeRequestSynchronously:[_httpManager newUnpairRequest]];
|
||||
[_callback pairFailed:@"Pairing stage #5 failed"];
|
||||
return;
|
||||
}
|
||||
[_callback pairSuccessful];
|
||||
}
|
||||
|
||||
- (NSData*) concatData:(NSData*)data with:(NSData*)moreData {
|
||||
NSMutableData* concatData = [[NSMutableData alloc] initWithData:data];
|
||||
[concatData appendData:moreData];
|
||||
return concatData;
|
||||
}
|
||||
|
||||
- (NSString*) generatePIN {
|
||||
NSString* PIN = [NSString stringWithFormat:@"%d%d%d%d",
|
||||
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
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Connection.h
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/19/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol ConnectionCallbacks <NSObject>
|
||||
|
||||
- (void) connectionStarted;
|
||||
- (void) connectionTerminated:(long)errorCode;
|
||||
- (void) stageStarting:(char*)stageName;
|
||||
- (void) stageComplete:(char*)stageName;
|
||||
- (void) stageFailed:(char*)stageName withError:(long)errorCode;
|
||||
- (void) launchFailed:(NSString*)message;
|
||||
- (void) displayMessage:(char*)message;
|
||||
- (void) displayTransientMessage:(char*)message;
|
||||
|
||||
@end
|
||||
|
||||
@interface Connection : NSOperation <NSStreamDelegate>
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
|
||||
-(void) terminate;
|
||||
-(void) main;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,416 @@
|
||||
//
|
||||
// Connection.m
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/19/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Connection.h"
|
||||
|
||||
#import <AudioUnit/AudioUnit.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#include "Limelight.h"
|
||||
#include "opus.h"
|
||||
|
||||
@implementation Connection {
|
||||
IP_ADDRESS _host;
|
||||
STREAM_CONFIGURATION _streamConfig;
|
||||
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
|
||||
DECODER_RENDERER_CALLBACKS _drCallbacks;
|
||||
AUDIO_RENDERER_CALLBACKS _arCallbacks;
|
||||
}
|
||||
|
||||
static OpusDecoder *opusDecoder;
|
||||
static id<ConnectionCallbacks> _callbacks;
|
||||
|
||||
#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;
|
||||
|
||||
void DrSetup(int width, int height, int fps, void* context, int drFlags)
|
||||
{
|
||||
}
|
||||
|
||||
void DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
{
|
||||
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
|
||||
if (data != NULL) {
|
||||
int offset = 0;
|
||||
|
||||
PLENTRY entry = decodeUnit->bufferList;
|
||||
while (entry != NULL) {
|
||||
memcpy(&data[offset], entry->data, entry->length);
|
||||
offset += entry->length;
|
||||
entry = entry->next;
|
||||
}
|
||||
|
||||
// This function will take our buffer
|
||||
[renderer submitDecodeBuffer:data length:decodeUnit->fullLength];
|
||||
}
|
||||
}
|
||||
|
||||
void DrStart(void)
|
||||
{
|
||||
}
|
||||
|
||||
void DrStop(void)
|
||||
{
|
||||
}
|
||||
|
||||
void DrRelease(void)
|
||||
{
|
||||
}
|
||||
|
||||
void ArInit(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
opusDecoder = opus_decoder_create(48000, 2, &err);
|
||||
|
||||
audioLock = [[NSLock alloc] init];
|
||||
|
||||
// Configure the audio session for our app
|
||||
NSError *audioSessionError = nil;
|
||||
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
||||
|
||||
[audioSession setPreferredSampleRate:48000.0 error:&audioSessionError];
|
||||
[audioSession setCategory: AVAudioSessionCategoryPlayback error: &audioSessionError];
|
||||
[audioSession setPreferredOutputNumberOfChannels:2 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) {
|
||||
printf("Unable to instantiate new AudioComponent: %d\n", (int32_t)status);
|
||||
}
|
||||
|
||||
AudioStreamBasicDescription audioFormat = {0};
|
||||
audioFormat.mSampleRate = 48000;
|
||||
audioFormat.mBitsPerChannel = 16;
|
||||
audioFormat.mFormatID = kAudioFormatLinearPCM;
|
||||
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
|
||||
audioFormat.mChannelsPerFrame = 2;
|
||||
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) {
|
||||
printf("Unable to set audio unit to input: %d\n", (int32_t)status);
|
||||
}
|
||||
|
||||
AURenderCallbackStruct callbackStruct = {0};
|
||||
callbackStruct.inputProc = playbackCallback;
|
||||
callbackStruct.inputProcRefCon = NULL;
|
||||
|
||||
status = AudioUnitSetProperty(audioUnit,
|
||||
kAudioUnitProperty_SetRenderCallback,
|
||||
kAudioUnitScope_Input,
|
||||
OUTPUT_BUS,
|
||||
&callbackStruct,
|
||||
sizeof(callbackStruct));
|
||||
if (status) {
|
||||
printf("Unable to set audio unit callback: %d\n", (int32_t)status);
|
||||
}
|
||||
|
||||
status = AudioUnitInitialize(audioUnit);
|
||||
if (status) {
|
||||
printf("Unable to initialize audioUnit: %d\n", (int32_t)status);
|
||||
}
|
||||
}
|
||||
|
||||
void ArRelease(void)
|
||||
{
|
||||
if (opusDecoder != NULL) {
|
||||
opus_decoder_destroy(opusDecoder);
|
||||
opusDecoder = NULL;
|
||||
}
|
||||
|
||||
OSStatus status = AudioUnitUninitialize(audioUnit);
|
||||
if (status) {
|
||||
printf("Unable to uninitialize audioUnit: %d\n", (int32_t)status);
|
||||
}
|
||||
|
||||
// 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 ArStart(void)
|
||||
{
|
||||
OSStatus status = AudioOutputUnitStart(audioUnit);
|
||||
if (status) {
|
||||
printf("Unable to start audioUnit: %d\n", (int32_t)status);
|
||||
}
|
||||
}
|
||||
|
||||
void ArStop(void)
|
||||
{
|
||||
OSStatus status = AudioOutputUnitStop(audioUnit);
|
||||
if (status) {
|
||||
printf("Unable to stop audioUnit: %d\n", (int32_t)status);
|
||||
}
|
||||
}
|
||||
|
||||
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
{
|
||||
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;
|
||||
|
||||
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) {
|
||||
printf("Audio player too slow. Dropping all decoded samples!\n");
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClStageStarting(int stage)
|
||||
{
|
||||
[_callbacks stageStarting:(char*)LiGetStageName(stage)];
|
||||
}
|
||||
|
||||
void ClStageComplete(int stage)
|
||||
{
|
||||
[_callbacks stageComplete:(char*)LiGetStageName(stage)];
|
||||
}
|
||||
|
||||
void ClStageFailed(int stage, long errorCode)
|
||||
{
|
||||
[_callbacks stageFailed:(char*)LiGetStageName(stage) withError:errorCode];
|
||||
}
|
||||
|
||||
void ClConnectionStarted(void)
|
||||
{
|
||||
[_callbacks connectionStarted];
|
||||
}
|
||||
|
||||
void ClConnectionTerminated(long errorCode)
|
||||
{
|
||||
[_callbacks connectionTerminated: errorCode];
|
||||
}
|
||||
|
||||
void ClDisplayMessage(char* message)
|
||||
{
|
||||
[_callbacks displayMessage: message];
|
||||
}
|
||||
|
||||
void ClDisplayTransientMessage(char* message)
|
||||
{
|
||||
[_callbacks displayTransientMessage: message];
|
||||
}
|
||||
|
||||
-(void) terminate
|
||||
{
|
||||
// We dispatch this async to get out because this can be invoked
|
||||
// on a thread inside common and we don't want to deadlock
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// This is safe to call even before LiStartConnection
|
||||
LiStopConnection();
|
||||
});
|
||||
}
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks
|
||||
{
|
||||
self = [super init];
|
||||
_host = config.hostAddr;
|
||||
renderer = myRenderer;
|
||||
_callbacks = callbacks;
|
||||
_streamConfig.width = config.width;
|
||||
_streamConfig.height = config.height;
|
||||
_streamConfig.fps = config.frameRate;
|
||||
_streamConfig.bitrate = config.bitRate;
|
||||
_streamConfig.packetSize = 1024;
|
||||
|
||||
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));
|
||||
|
||||
_drCallbacks.setup = DrSetup;
|
||||
_drCallbacks.start = DrStart;
|
||||
_drCallbacks.stop = DrStop;
|
||||
_drCallbacks.release = DrRelease;
|
||||
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
|
||||
|
||||
_arCallbacks.init = ArInit;
|
||||
_arCallbacks.start = ArStart;
|
||||
_arCallbacks.stop = ArStop;
|
||||
_arCallbacks.release = ArRelease;
|
||||
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
||||
|
||||
_clCallbacks.stageStarting = ClStageStarting;
|
||||
_clCallbacks.stageComplete = ClStageComplete;
|
||||
_clCallbacks.stageFailed = ClStageFailed;
|
||||
_clCallbacks.connectionStarted = ClConnectionStarted;
|
||||
_clCallbacks.connectionTerminated = ClConnectionTerminated;
|
||||
_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
|
||||
{
|
||||
LiStartConnection(_host, &_streamConfig, &_clCallbacks, &_drCallbacks, &_arCallbacks, NULL, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// StreamConfiguration.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface StreamConfiguration : NSObject
|
||||
|
||||
@property NSString* host;
|
||||
@property int hostAddr;
|
||||
@property int width;
|
||||
@property int height;
|
||||
@property int frameRate;
|
||||
@property int bitRate;
|
||||
@property int riKeyId;
|
||||
@property NSData* riKey;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// StreamConfiguration.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@implementation StreamConfiguration
|
||||
@synthesize host, hostAddr, width, height, frameRate, bitRate, riKeyId, riKey;
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// StreamManager.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "StreamConfiguration.h"
|
||||
#import "Connection.h"
|
||||
|
||||
@interface StreamManager : NSOperation
|
||||
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
||||
- (void) stopStream;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// StreamManager.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamManager.h"
|
||||
#import "CryptoManager.h"
|
||||
#import "HttpManager.h"
|
||||
#import "Utils.h"
|
||||
|
||||
@implementation StreamManager {
|
||||
StreamConfiguration* _config;
|
||||
UIView* _renderView;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
Connection* _connection;
|
||||
}
|
||||
|
||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
|
||||
self = [super init];
|
||||
_config = config;
|
||||
_renderView = view;
|
||||
_callbacks = callbacks;
|
||||
_config.riKey = [Utils randomBytes:16];
|
||||
_config.riKeyId = arc4random();
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)main {
|
||||
[CryptoManager generateKeyPairUsingSSl];
|
||||
NSString* uniqueId = [CryptoManager getUniqueID];
|
||||
NSData* cert = [CryptoManager readCertFromFile];
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
|
||||
uniqueId:uniqueId
|
||||
deviceName:@"roth"
|
||||
cert:cert];
|
||||
|
||||
NSData* serverInfoResp = [hMan executeRequestSynchronously:[hMan newServerInfoRequest]];
|
||||
NSString* currentGame = [HttpManager getStringFromXML:serverInfoResp tag:@"currentgame"];
|
||||
NSString* pairStatus = [HttpManager getStringFromXML:serverInfoResp tag:@"PairStatus"];
|
||||
if (currentGame == NULL || pairStatus == NULL) {
|
||||
[_callbacks launchFailed:@"Failed to connect to PC"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (![pairStatus isEqualToString:@"1"]) {
|
||||
// Not paired
|
||||
[_callbacks launchFailed:@"Device not paired to PC"];
|
||||
return;
|
||||
}
|
||||
|
||||
// resumeApp and launchApp handle calling launchFailed
|
||||
if (![currentGame isEqualToString:@"0"]) {
|
||||
// App already running, resume it
|
||||
if (![self resumeApp:hMan]) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Start app
|
||||
if (![self launchApp:hMan]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
[_connection terminate];
|
||||
}
|
||||
|
||||
- (BOOL) launchApp:(HttpManager*)hMan {
|
||||
NSData* launchResp = [hMan executeRequestSynchronously:
|
||||
[hMan newLaunchRequest:@"67339056"
|
||||
width:_config.width
|
||||
height:_config.height
|
||||
refreshRate:_config.frameRate
|
||||
rikey:[Utils bytesToHex:_config.riKey]
|
||||
rikeyid:_config.riKeyId]];
|
||||
NSString *gameSession = [HttpManager getStringFromXML:launchResp tag:@"gamesession"];
|
||||
if (launchResp == NULL) {
|
||||
[_callbacks launchFailed:@"Failed to launch app"];
|
||||
return FALSE;
|
||||
} else if (gameSession == NULL || [gameSession isEqualToString:@"0"]) {
|
||||
[_callbacks launchFailed:[HttpManager getStatusStringFromXML:launchResp]];
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
- (BOOL) resumeApp:(HttpManager*)hMan {
|
||||
NSData* resumeResp = [hMan executeRequestSynchronously:
|
||||
[hMan newResumeRequestWithRiKey:[Utils bytesToHex:_config.riKey]
|
||||
riKeyId:_config.riKeyId]];
|
||||
NSString *resume = [HttpManager getStringFromXML:resumeResp tag:@"resume"];
|
||||
if (resumeResp == NULL) {
|
||||
[_callbacks launchFailed:@"Failed to resume app"];
|
||||
return FALSE;
|
||||
} else if (resume == NULL || [resume isEqualToString:@"0"]) {
|
||||
[_callbacks launchFailed:[HttpManager getStatusStringFromXML:resumeResp]];
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// VideoDecoderRenderer.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/18/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
@interface VideoDecoderRenderer : NSObject
|
||||
|
||||
- (id)initWithView:(UIView*)view;
|
||||
|
||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
|
||||
|
||||
- (void)submitDecodeBuffer:(unsigned char *)data length:(int)length;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,236 @@
|
||||
//
|
||||
// VideoDecoderRenderer.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Cameron Gutman on 10/18/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "VideoDecoderRenderer.h"
|
||||
|
||||
@implementation VideoDecoderRenderer {
|
||||
AVSampleBufferDisplayLayer* displayLayer;
|
||||
Boolean waitingForSps, waitingForPps;
|
||||
|
||||
NSData *spsData, *ppsData;
|
||||
CMVideoFormatDescriptionRef formatDesc;
|
||||
}
|
||||
|
||||
- (id)initWithView:(UIView*)view
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||
displayLayer.bounds = view.bounds;
|
||||
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
|
||||
displayLayer.position = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds));
|
||||
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||
[view.layer addSublayer:displayLayer];
|
||||
|
||||
// We need some parameter sets before we can properly start decoding frames
|
||||
waitingForSps = true;
|
||||
waitingForPps = true;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#define FRAME_START_PREFIX_SIZE 4
|
||||
#define NALU_START_PREFIX_SIZE 3
|
||||
|
||||
#define NAL_LENGTH_PREFIX_SIZE 4
|
||||
|
||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
|
||||
{
|
||||
OSStatus status;
|
||||
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
|
||||
|
||||
// If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
|
||||
// so it can handle freeing it when the block buffer is destroyed
|
||||
if (offset == 1) {
|
||||
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
|
||||
|
||||
// Pass the real buffer pointer directly (no offset)
|
||||
// This will give it to the block buffer to free when it's released.
|
||||
// All further calls to CMBlockBufferAppendMemoryBlock will do so
|
||||
// at an offset and will not be asking the buffer to be freed.
|
||||
status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
|
||||
nalLength + 1, // Add 1 for the offset we decremented
|
||||
kCFAllocatorDefault,
|
||||
NULL, 0, nalLength + 1, 0);
|
||||
if (status != noErr) {
|
||||
printf("CMBlockBufferReplaceDataBytes failed: %d\n", (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) {
|
||||
printf("CMBlockBufferReplaceDataBytes failed: %d\n", (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) {
|
||||
printf("CMBlockBufferAppendMemoryBlock failed: %d\n", (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) {
|
||||
printf("CMBlockBufferReplaceDataBytes failed: %d\n", (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) {
|
||||
printf("CMBlockBufferReplaceDataBytes failed: %d\n", (int)status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function must free data
|
||||
- (void)submitDecodeBuffer:(unsigned char *)data length:(int)length
|
||||
{
|
||||
unsigned char nalType = data[FRAME_START_PREFIX_SIZE] & 0x1F;
|
||||
OSStatus status;
|
||||
|
||||
if (nalType == 0x7 || nalType == 0x8) {
|
||||
if (nalType == 0x7) {
|
||||
printf("Got SPS\n");
|
||||
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 == 0x8) {
|
||||
printf("Got PPS\n");
|
||||
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] };
|
||||
|
||||
printf("Constructing new format description\n");
|
||||
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
|
||||
2, /* count of parameter sets */
|
||||
parameterSetPointers,
|
||||
parameterSetSizes,
|
||||
NAL_LENGTH_PREFIX_SIZE,
|
||||
&formatDesc);
|
||||
if (status != noErr) {
|
||||
printf("Failed to create format description: %d\n", (int)status);
|
||||
formatDesc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Free the data buffer
|
||||
free(data);
|
||||
|
||||
// No frame data to submit for these NALUs
|
||||
return;
|
||||
}
|
||||
|
||||
if (formatDesc == NULL) {
|
||||
// Can't decode if we haven't gotten our parameter sets yet
|
||||
free(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (nalType != 0x1 && nalType != 0x5) {
|
||||
// Don't submit parameter set data
|
||||
free(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now we're decoding actual frame data here
|
||||
CMBlockBufferRef blockBuffer;
|
||||
|
||||
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
|
||||
if (status != noErr) {
|
||||
printf("CMBlockBufferCreateEmpty failed: %d\n", (int)status);
|
||||
free(data);
|
||||
return;
|
||||
}
|
||||
|
||||
int lastOffset = -1;
|
||||
for (int i = 0; i < length - FRAME_START_PREFIX_SIZE; i++) {
|
||||
// Search for a NALU
|
||||
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
|
||||
// It's the start of a new NALU
|
||||
if (lastOffset != -1) {
|
||||
// We've seen a start before this so enqueue that NALU
|
||||
[self updateBufferForRange:blockBuffer data:data offset:lastOffset length:i - lastOffset];
|
||||
}
|
||||
|
||||
lastOffset = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastOffset != -1) {
|
||||
// Enqueue the remaining data
|
||||
[self updateBufferForRange:blockBuffer data:data offset:lastOffset length:length - lastOffset];
|
||||
}
|
||||
|
||||
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
|
||||
status = CMSampleBufferCreate(kCFAllocatorDefault,
|
||||
blockBuffer,
|
||||
true, NULL,
|
||||
NULL, formatDesc, 1, 0,
|
||||
NULL, 0, NULL,
|
||||
&sampleBuffer);
|
||||
if (status != noErr) {
|
||||
printf("CMSampleBufferCreate failed: %d\n", (int)status);
|
||||
CFRelease(blockBuffer);
|
||||
return;
|
||||
}
|
||||
|
||||
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(blockBuffer);
|
||||
CFRelease(sampleBuffer);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Computer.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/14/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Computer : NSObject
|
||||
@property NSString* displayName;
|
||||
@property NSString* hostName;
|
||||
@property BOOL paired;
|
||||
|
||||
- (id) initWithHost:(NSNetService*)host;
|
||||
- (id) initWithIp:(NSString*)host;
|
||||
- (id) initPlaceholder;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Computer.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/14/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Computer.h"
|
||||
|
||||
@implementation Computer
|
||||
|
||||
- (id) initWithHost:(NSNetService *)host {
|
||||
self = [super init];
|
||||
|
||||
self.hostName = [host hostName];
|
||||
self.displayName = [host name];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initWithIp:(NSString*)host {
|
||||
self = [super init];
|
||||
|
||||
self.hostName = host;
|
||||
self.displayName = host;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id) initPlaceholder {
|
||||
self = [super init];
|
||||
|
||||
self.hostName = NULL;
|
||||
self.displayName = @"No computers found";
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Utils.h
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface Utils : NSObject
|
||||
|
||||
+ (NSData*) randomBytes:(NSInteger)length;
|
||||
+ (NSString*) bytesToHex:(NSData*)data;
|
||||
+ (NSData*) hexToBytes:(NSString*) hex;
|
||||
+ (int) resolveHost:(NSString*)host;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Utils.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/20/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Utils.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netdb.h>
|
||||
|
||||
@implementation Utils
|
||||
|
||||
+ (NSData*) randomBytes:(NSInteger)length {
|
||||
char* bytes = malloc(length);
|
||||
arc4random_buf(bytes, length);
|
||||
NSData* randomData = [NSData dataWithBytes:bytes length:length];
|
||||
free(bytes);
|
||||
return randomData;
|
||||
}
|
||||
|
||||
+ (NSData*) hexToBytes:(NSString*) hex {
|
||||
unsigned long len = [hex length];
|
||||
NSMutableData* data = [NSMutableData dataWithCapacity:len / 2];
|
||||
char byteChars[3] = {'\0','\0','\0'};
|
||||
unsigned long wholeByte;
|
||||
|
||||
const char *chars = [hex UTF8String];
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
byteChars[0] = chars[i++];
|
||||
byteChars[1] = chars[i++];
|
||||
wholeByte = strtoul(byteChars, NULL, 16);
|
||||
[data appendBytes:&wholeByte length:1];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (NSString*) bytesToHex:(NSData*)data {
|
||||
const unsigned char* bytes = [data bytes];
|
||||
NSMutableString *hex = [[NSMutableString alloc] init];
|
||||
for (int i = 0; i < [data length]; i++) {
|
||||
[hex appendFormat:@"%02X" , bytes[i]];
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
+ (int) resolveHost:(NSString*)host {
|
||||
struct hostent *hostent;
|
||||
|
||||
if (inet_addr([host UTF8String]) != INADDR_NONE) {
|
||||
// Already an IP address
|
||||
int addr = inet_addr([host UTF8String]);
|
||||
NSLog(@"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]);
|
||||
NSLog(@"Resolved %@ -> %s", host, ipstr);
|
||||
int addr = inet_addr(ipstr);
|
||||
NSLog(@"host address: %d", addr);
|
||||
return addr;
|
||||
} else {
|
||||
NSLog(@"Failed to resolve host: %d", h_errno);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// MainFrameViewController.h
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/17/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "MDNSManager.h"
|
||||
#import "PairManager.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@interface MainFrameViewController : UIViewController <UIPickerViewDataSource, UIPickerViewDelegate, MDNSCallback, NSURLConnectionDelegate, PairCallback, UITextFieldDelegate>
|
||||
@property (strong, nonatomic) IBOutlet UIPickerView *HostPicker;
|
||||
- (IBAction)StreamButton:(UIButton *)sender;
|
||||
- (IBAction)PairButton:(UIButton *)sender;
|
||||
|
||||
@property (strong, nonatomic) IBOutlet UIPickerView *StreamConfigs;
|
||||
@property (strong, nonatomic) NSArray* streamConfigVals;
|
||||
@property (strong, nonatomic) NSArray* hostPickerVals;
|
||||
@property (strong, nonatomic) IBOutlet UITextField *hostTextField;
|
||||
|
||||
+ (StreamConfiguration*) getStreamConfiguration;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,278 @@
|
||||
//
|
||||
// MainFrameViewController.m
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/17/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MainFrameViewController.h"
|
||||
#import "Computer.h"
|
||||
#import "CryptoManager.h"
|
||||
#import "HttpManager.h"
|
||||
#import "Connection.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "Utils.h"
|
||||
|
||||
@implementation MainFrameViewController {
|
||||
NSOperationQueue* _opQueue;
|
||||
MDNSManager* _mDNSManager;
|
||||
Computer* _selectedHost;
|
||||
UIAlertView* _pairAlert;
|
||||
}
|
||||
static StreamConfiguration* streamConfig;
|
||||
|
||||
+ (StreamConfiguration*) getStreamConfiguration {
|
||||
return streamConfig;
|
||||
}
|
||||
|
||||
- (void)PairButton:(UIButton *)sender
|
||||
{
|
||||
NSLog(@"Pair Button Pressed!");
|
||||
if ([self.hostTextField.text length] > 0) {
|
||||
_selectedHost = [[Computer alloc] initWithIp:self.hostTextField.text];
|
||||
NSLog(@"Using custom host: %@", self.hostTextField.text);
|
||||
}
|
||||
|
||||
if (![self validatePcSelected]) {
|
||||
NSLog(@"No valid PC selected");
|
||||
return;
|
||||
}
|
||||
|
||||
[CryptoManager generateKeyPairUsingSSl];
|
||||
NSString* uniqueId = [CryptoManager getUniqueID];
|
||||
NSData* cert = [CryptoManager readCertFromFile];
|
||||
|
||||
if ([Utils resolveHost:_selectedHost.hostName] == 0) {
|
||||
[self displayDnsFailedDialog];
|
||||
return;
|
||||
}
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_selectedHost.hostName uniqueId:uniqueId deviceName:@"roth" cert:cert];
|
||||
PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:cert callback:self];
|
||||
|
||||
[_opQueue addOperation:pMan];
|
||||
}
|
||||
|
||||
- (void)showPIN:(NSString *)PIN {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
_pairAlert = [[UIAlertView alloc] initWithTitle:@"Pairing" message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
|
||||
[_pairAlert show];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)pairFailed:(NSString *)message {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[_pairAlert dismissWithClickedButtonIndex:0 animated:NO];
|
||||
_pairAlert = [[UIAlertView alloc] initWithTitle:@"Pairing Failed" message:message delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
|
||||
[_pairAlert show];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)pairSuccessful {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[_pairAlert dismissWithClickedButtonIndex:0 animated:NO];
|
||||
_pairAlert = [[UIAlertView alloc] initWithTitle:@"Pairing Succesful" message:@"Successfully paired to host" delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
|
||||
[_pairAlert show];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)displayDnsFailedDialog {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Network Error"
|
||||
message:@"Failed to resolve host."
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)StreamButton:(UIButton *)sender
|
||||
{
|
||||
NSLog(@"Stream Button Pressed!");
|
||||
if ([self.hostTextField.text length] > 0) {
|
||||
_selectedHost = [[Computer alloc] initWithIp:self.hostTextField.text];
|
||||
NSLog(@"Using custom host: %@", self.hostTextField.text);
|
||||
}
|
||||
|
||||
if (![self validatePcSelected]) {
|
||||
NSLog(@"No valid PC selected");
|
||||
return;
|
||||
}
|
||||
|
||||
streamConfig = [[StreamConfiguration alloc] init];
|
||||
streamConfig.host = _selectedHost.hostName;
|
||||
streamConfig.hostAddr = [Utils resolveHost:_selectedHost.hostName];
|
||||
if (streamConfig.hostAddr == 0) {
|
||||
[self displayDnsFailedDialog];
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long selectedConf = [self.StreamConfigs selectedRowInComponent:0];
|
||||
NSLog(@"selectedConf: %ld", selectedConf);
|
||||
switch (selectedConf) {
|
||||
case 0:
|
||||
streamConfig.width = 1280;
|
||||
streamConfig.height = 720;
|
||||
streamConfig.frameRate = 30;
|
||||
streamConfig.bitRate = 5000;
|
||||
break;
|
||||
default:
|
||||
case 1:
|
||||
streamConfig.width = 1280;
|
||||
streamConfig.height = 720;
|
||||
streamConfig.frameRate = 60;
|
||||
streamConfig.bitRate = 10000;
|
||||
break;
|
||||
case 2:
|
||||
streamConfig.width = 1920;
|
||||
streamConfig.height = 1080;
|
||||
streamConfig.frameRate = 30;
|
||||
streamConfig.bitRate = 10000;
|
||||
break;
|
||||
case 3:
|
||||
streamConfig.width = 1920;
|
||||
streamConfig.height = 1080;
|
||||
streamConfig.frameRate = 60;
|
||||
streamConfig.bitRate = 20000;
|
||||
break;
|
||||
}
|
||||
NSLog(@"StreamConfig: %@, %d, %dx%dx%d at %d Mbps", streamConfig.host, streamConfig.hostAddr, streamConfig.width, streamConfig.height, streamConfig.frameRate, streamConfig.bitRate);
|
||||
[self performSegueWithIdentifier:@"createStreamFrame" sender:self];
|
||||
}
|
||||
|
||||
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
|
||||
{
|
||||
if (pickerView == self.StreamConfigs) {
|
||||
return [self.streamConfigVals objectAtIndex:row];
|
||||
} else if (pickerView == self.HostPicker) {
|
||||
return ((Computer*)([self.hostPickerVals objectAtIndex:row])).displayName;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedHost:(NSInteger)selectedIndex
|
||||
{
|
||||
_selectedHost = (Computer*)([self.hostPickerVals objectAtIndex:selectedIndex]);
|
||||
if (_selectedHost.hostName == NULL) {
|
||||
// This must be the placeholder computer
|
||||
_selectedHost = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
|
||||
{
|
||||
if (pickerView == self.HostPicker) {
|
||||
[self setSelectedHost:[self.HostPicker selectedRowInComponent:0]];
|
||||
}
|
||||
|
||||
//TODO: figure out how to save this info!!
|
||||
}
|
||||
|
||||
// returns the number of 'columns' to display.
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// returns the # of rows in each component..
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
|
||||
{
|
||||
if (pickerView == self.StreamConfigs) {
|
||||
return self.streamConfigVals.count;
|
||||
} else if (pickerView == self.HostPicker) {
|
||||
return self.hostPickerVals.count;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.streamConfigVals = [[NSArray alloc] initWithObjects:@"1280x720 (30Hz)", @"1280x720 (60Hz)", @"1920x1080 (30Hz)", @"1920x1080 (60Hz)",nil];
|
||||
[self.StreamConfigs selectRow:1 inComponent:0 animated:NO];
|
||||
|
||||
_opQueue = [[NSOperationQueue alloc] init];
|
||||
|
||||
// Initialize the host picker list
|
||||
[self updateHosts:[[NSArray alloc] init]];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
_mDNSManager = [[MDNSManager alloc] initWithCallback:self];
|
||||
[_mDNSManager searchForHosts];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewDidDisappear:animated];
|
||||
[_mDNSManager stopSearching];
|
||||
}
|
||||
|
||||
- (void)updateHosts:(NSArray *)hosts {
|
||||
NSMutableArray *hostPickerValues = [[NSMutableArray alloc] initWithArray:hosts];
|
||||
|
||||
if ([hostPickerValues count] == 0) {
|
||||
[hostPickerValues addObject:[[Computer alloc] initPlaceholder]];
|
||||
}
|
||||
|
||||
self.hostPickerVals = hostPickerValues;
|
||||
[self.HostPicker reloadAllComponents];
|
||||
|
||||
[self setSelectedHost:[self.HostPicker selectedRowInComponent:0]];
|
||||
}
|
||||
|
||||
- (BOOL)validatePcSelected {
|
||||
if (_selectedHost == NULL) {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"No PC Selected"
|
||||
message:@"You must select a PC to continue."
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:nil]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[self.view endEditing:YES];
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
[textField resignFirstResponder];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSUInteger)supportedInterfaceOrientations {
|
||||
NSString *deviceType = [UIDevice currentDevice].model;
|
||||
if ([deviceType containsString:@"iPhone"] || [deviceType containsString:@"iPod"]) {
|
||||
return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown;
|
||||
} else {
|
||||
return UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight;
|
||||
}
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
NSString *deviceType = [UIDevice currentDevice].model;
|
||||
if ([deviceType containsString:@"iPhone"] || [deviceType containsString:@"iPod"]) {
|
||||
return UIInterfaceOrientationPortrait;
|
||||
} else {
|
||||
return UIInterfaceOrientationLandscapeRight;
|
||||
}
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// StreamFrameViewController.h
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/18/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Connection.h"
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks>
|
||||
@property (strong, nonatomic) IBOutlet UILabel *stageLabel;
|
||||
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// StreamFrameViewController.m
|
||||
// Limelight-iOS
|
||||
//
|
||||
// Created by Diego Waxemberg on 1/18/14.
|
||||
// Copyright (c) 2014 Diego Waxemberg. All rights reserved.
|
||||
//
|
||||
|
||||
#import "StreamFrameViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "ControllerSupport.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
@implementation StreamFrameViewController {
|
||||
ControllerSupport *_controllerSupport;
|
||||
StreamManager *_streamMan;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.stageLabel setText:@"Starting App"];
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||||
|
||||
_controllerSupport = [[ControllerSupport alloc] init];
|
||||
|
||||
_streamMan = [[StreamManager alloc] initWithConfig:[MainFrameViewController getStreamConfiguration]
|
||||
renderView:self.view
|
||||
connectionCallbacks:self];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:_streamMan];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationWillResignActive:)
|
||||
name:UIApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
[_streamMan stopStream];
|
||||
[self performSegueWithIdentifier:@"returnToMainFrame" sender:self];
|
||||
}
|
||||
|
||||
- (void) connectionStarted {
|
||||
printf("Connection started\n");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.spinner stopAnimating];
|
||||
[self.stageLabel setText:@"Waiting for first frame..."];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectionTerminated:(long)errorCode {
|
||||
printf("Connection terminated: %ld\n", errorCode);
|
||||
|
||||
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:@"Connection Terminated" message:@"The connection terminated unexpectedly" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
|
||||
[self performSegueWithIdentifier:@"returnToMainFrame" sender:self];
|
||||
}]];
|
||||
[self presentViewController:conTermAlert animated:YES completion:nil];
|
||||
|
||||
[_streamMan stopStream];
|
||||
}
|
||||
|
||||
- (void) stageStarting:(char*)stageName {
|
||||
printf("Starting %s\n", stageName);
|
||||
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];
|
||||
});
|
||||
}
|
||||
|
||||
- (void) stageComplete:(char*)stageName {
|
||||
}
|
||||
|
||||
- (void) stageFailed:(char*)stageName withError:(long)errorCode {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
message:[NSString stringWithFormat:@"%s failed with error %ld",
|
||||
stageName, errorCode]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
|
||||
[self performSegueWithIdentifier:@"returnToMainFrame" sender:self];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void) launchFailed:(NSString*)message {
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
|
||||
[self performSegueWithIdentifier:@"returnToMainFrame" sender:self];
|
||||
}]];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void) displayMessage:(char*)message {
|
||||
printf("Display message: %s\n", message);
|
||||
}
|
||||
|
||||
- (void) displayTransientMessage:(char*)message {
|
||||
printf("Display transient message: %s\n", message);
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSUInteger)supportedInterfaceOrientations {
|
||||
return UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
|
||||
return UIInterfaceOrientationLandscapeRight;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// main.m
|
||||
// Limelight
|
||||
//
|
||||
// Created by Diego Waxemberg on 8/30/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.limelight-stream.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// LimelightTests.m
|
||||
// LimelightTests
|
||||
//
|
||||
// Created by Diego Waxemberg on 8/30/14.
|
||||
// Copyright (c) 2014 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
@interface LimelightTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation LimelightTests
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
[super setUp];
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
{
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
- (void)testExample
|
||||
{
|
||||
XCTFail(@"No implementation for \"%s\"", __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" initialViewController="wb7-af-jn8">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Main Frame View Controller-->
|
||||
<scene sceneID="Me4-Nr-liz">
|
||||
<objects>
|
||||
<viewController id="wb7-af-jn8" customClass="MainFrameViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="9ZL-gP-cfY"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="Shd-7C-CVq"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" horizontalHuggingPriority="222" id="ugc-rC-3gm">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1024" height="768"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bJR-sz-8uh">
|
||||
<rect key="frame" x="0.0" y="80" width="1024" height="216"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="wb7-af-jn8" id="EW1-pG-aj0"/>
|
||||
<outlet property="delegate" destination="wb7-af-jn8" id="Piq-cV-gxM"/>
|
||||
</connections>
|
||||
</pickerView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="taW-Um-zc6" userLabel="Start Stream Btn">
|
||||
<rect key="frame" x="317" y="273" width="391" height="141"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="391" id="f7x-eA-edG"/>
|
||||
<constraint firstAttribute="height" constant="141" id="sdD-TL-lHS"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="26"/>
|
||||
<state key="normal" title="Start Streaming Steam!">
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="StreamButton:" destination="wb7-af-jn8" eventType="touchUpInside" id="u2m-8A-Tor"/>
|
||||
</connections>
|
||||
</button>
|
||||
<pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rnA-uG-hAA">
|
||||
<rect key="frame" x="0.0" y="380" width="1024" height="216"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="wb7-af-jn8" id="25E-op-oh6"/>
|
||||
<outlet property="delegate" destination="wb7-af-jn8" id="gjZ-oj-THD"/>
|
||||
</connections>
|
||||
</pickerView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0Nz-KX-Du4" userLabel="Pair Btn">
|
||||
<rect key="frame" x="439" y="595" width="147" height="36"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="36" id="C4p-5e-fbL"/>
|
||||
<constraint firstAttribute="width" constant="147" id="I0J-8h-40c"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="26"/>
|
||||
<state key="normal" title="Pair with PC">
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="PairButton:" destination="wb7-af-jn8" eventType="touchUpInside" id="Ct1-6m-aYg"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Geforce Host" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="OCq-Pr-kFE">
|
||||
<rect key="frame" x="233" y="235" width="601" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="601" id="haa-mB-l8k"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="wb7-af-jn8" id="YMu-2k-Dyk"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="taW-Um-zc6" firstAttribute="top" secondItem="OCq-Pr-kFE" secondAttribute="bottom" constant="8" symbolic="YES" id="7GU-HB-BX8"/>
|
||||
<constraint firstItem="OCq-Pr-kFE" firstAttribute="leading" secondItem="ugc-rC-3gm" secondAttribute="leading" constant="233" id="DM7-XL-gaO"/>
|
||||
<constraint firstAttribute="centerX" secondItem="rnA-uG-hAA" secondAttribute="centerX" id="EBV-fm-cGh"/>
|
||||
<constraint firstItem="taW-Um-zc6" firstAttribute="top" secondItem="ugc-rC-3gm" secondAttribute="top" constant="273" id="Gki-EV-x7L"/>
|
||||
<constraint firstItem="rnA-uG-hAA" firstAttribute="trailing" secondItem="bJR-sz-8uh" secondAttribute="trailing" id="RKH-ES-jQD"/>
|
||||
<constraint firstItem="rnA-uG-hAA" firstAttribute="leading" secondItem="bJR-sz-8uh" secondAttribute="leading" id="UNO-rw-Z5K"/>
|
||||
<constraint firstItem="taW-Um-zc6" firstAttribute="centerX" secondItem="rnA-uG-hAA" secondAttribute="centerX" id="clq-Co-82x"/>
|
||||
<constraint firstItem="Shd-7C-CVq" firstAttribute="top" secondItem="rnA-uG-hAA" secondAttribute="bottom" constant="172" id="gRA-PG-Iqy"/>
|
||||
<constraint firstItem="Shd-7C-CVq" firstAttribute="top" secondItem="0Nz-KX-Du4" secondAttribute="bottom" constant="137" id="nJf-9A-cfP"/>
|
||||
<constraint firstItem="rnA-uG-hAA" firstAttribute="centerX" secondItem="0Nz-KX-Du4" secondAttribute="centerX" id="toQ-ei-Pr0"/>
|
||||
<constraint firstItem="rnA-uG-hAA" firstAttribute="top" secondItem="bJR-sz-8uh" secondAttribute="bottom" constant="84" id="zMG-gd-Hxd"/>
|
||||
<constraint firstItem="rnA-uG-hAA" firstAttribute="leading" secondItem="ugc-rC-3gm" secondAttribute="leading" id="znq-P0-UWg"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
|
||||
<connections>
|
||||
<outlet property="HostPicker" destination="bJR-sz-8uh" id="pWB-P3-ZJ2"/>
|
||||
<outlet property="StreamConfigs" destination="rnA-uG-hAA" id="LPP-jQ-5eW"/>
|
||||
<outlet property="hostTextField" destination="OCq-Pr-kFE" id="HS2-KF-inc"/>
|
||||
<segue destination="OIm-0n-i9v" kind="modal" identifier="createStreamFrame" modalPresentationStyle="fullScreen" id="MOD-9A-3Sk"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ZYl-Xu-QyD" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-375" y="-162"/>
|
||||
</scene>
|
||||
<!--Stream Frame View Controller-->
|
||||
<scene sceneID="RuD-qk-7nb">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="MainFrame" id="OIm-0n-i9v" customClass="StreamFrameViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="NG3-N1-D4k"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="3MH-n6-BSR"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="VPm-Ae-rc4" userLabel="RenderView" customClass="StreamView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1024" height="768"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="iOs-1X-mSU">
|
||||
<rect key="frame" x="502" y="374" width="20" height="20"/>
|
||||
</activityIndicatorView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dDs-kT-po6">
|
||||
<rect key="frame" x="491" y="402" width="42" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="dDs-kT-po6" firstAttribute="top" secondItem="iOs-1X-mSU" secondAttribute="bottom" constant="8" id="VZj-wk-dHp"/>
|
||||
<constraint firstAttribute="centerY" secondItem="iOs-1X-mSU" secondAttribute="centerY" id="YAN-0k-Rds"/>
|
||||
<constraint firstAttribute="centerX" secondItem="dDs-kT-po6" secondAttribute="centerX" id="bLr-5Z-OH3"/>
|
||||
<constraint firstAttribute="centerX" secondItem="iOs-1X-mSU" secondAttribute="centerX" id="ruD-8B-gj3"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
|
||||
<simulatedScreenMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="spinner" destination="iOs-1X-mSU" id="LSl-lr-7fF"/>
|
||||
<outlet property="stageLabel" destination="dDs-kT-po6" id="ziI-M8-UVf"/>
|
||||
<segue destination="wb7-af-jn8" kind="modal" identifier="returnToMainFrame" id="Ryr-0y-o43"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="hON-k2-Efa" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1252" y="-312"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar" statusBarStyle="lightContent"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
||||
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="dgh-JZ-Q7z">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Main Frame View Controller-->
|
||||
<scene sceneID="emz-kO-L7q">
|
||||
<objects>
|
||||
<viewController id="dgh-JZ-Q7z" customClass="MainFrameViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="0HR-9w-kvY"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="2rs-P3-zn7"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="eqF-iA-Rqx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="J5M-pv-UHA">
|
||||
<rect key="frame" x="82" y="303" width="156" height="30"/>
|
||||
<state key="normal" title="Start Streaming Steam">
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="StreamButton:" destination="dgh-JZ-Q7z" eventType="touchUpInside" id="3Bj-I0-bKs"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ATh-Tf-xRL">
|
||||
<rect key="frame" x="137" y="341" width="46" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="46" id="mm8-gm-flQ"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Pair">
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="PairButton:" destination="dgh-JZ-Q7z" eventType="touchUpInside" id="jHN-B5-hsw"/>
|
||||
</connections>
|
||||
</button>
|
||||
<pickerView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0xb-lk-PHK">
|
||||
<rect key="frame" x="0.0" y="354" width="320" height="162"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="dgh-JZ-Q7z" id="R0Y-EX-neJ"/>
|
||||
<outlet property="delegate" destination="dgh-JZ-Q7z" id="mkk-oI-y7K"/>
|
||||
</connections>
|
||||
</pickerView>
|
||||
<pickerView contentMode="scaleToFill" verticalHuggingPriority="249" translatesAutoresizingMaskIntoConstraints="NO" id="nFR-to-qQs">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="216"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="dgh-JZ-Q7z" id="C28-Eh-A6L"/>
|
||||
<outlet property="delegate" destination="dgh-JZ-Q7z" id="dTy-8g-aj1"/>
|
||||
</connections>
|
||||
</pickerView>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Geforce Host" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="unc-nO-WPH">
|
||||
<rect key="frame" x="16" y="224" width="288" height="30"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="dgh-JZ-Q7z" id="DCy-4m-RHG"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="0xb-lk-PHK" firstAttribute="leading" secondItem="2rs-P3-zn7" secondAttribute="leading" id="350-bN-0q6"/>
|
||||
<constraint firstAttribute="centerX" secondItem="J5M-pv-UHA" secondAttribute="centerX" id="5jf-C1-faJ"/>
|
||||
<constraint firstItem="unc-nO-WPH" firstAttribute="top" secondItem="0HR-9w-kvY" secondAttribute="bottom" constant="224" id="A1r-Mh-dg7"/>
|
||||
<constraint firstAttribute="centerX" secondItem="0xb-lk-PHK" secondAttribute="centerX" id="By0-u3-3MA"/>
|
||||
<constraint firstItem="0xb-lk-PHK" firstAttribute="centerX" secondItem="J5M-pv-UHA" secondAttribute="centerX" id="Gab-EA-Il5"/>
|
||||
<constraint firstItem="unc-nO-WPH" firstAttribute="leading" secondItem="J5M-pv-UHA" secondAttribute="leading" constant="-66" id="Qk0-0J-79M"/>
|
||||
<constraint firstItem="0xb-lk-PHK" firstAttribute="top" secondItem="J5M-pv-UHA" secondAttribute="bottom" constant="21" id="RNw-zD-7u7"/>
|
||||
<constraint firstItem="0xb-lk-PHK" firstAttribute="top" secondItem="ATh-Tf-xRL" secondAttribute="bottom" constant="-17" id="TN4-kv-Yli"/>
|
||||
<constraint firstItem="J5M-pv-UHA" firstAttribute="top" secondItem="nFR-to-qQs" secondAttribute="bottom" constant="87" id="Vcz-ix-cdp"/>
|
||||
<constraint firstItem="nFR-to-qQs" firstAttribute="top" secondItem="0HR-9w-kvY" secondAttribute="bottom" id="XWH-Vw-VfH"/>
|
||||
<constraint firstItem="0xb-lk-PHK" firstAttribute="centerX" secondItem="ATh-Tf-xRL" secondAttribute="centerX" id="bEv-zi-oip"/>
|
||||
<constraint firstItem="J5M-pv-UHA" firstAttribute="top" secondItem="unc-nO-WPH" secondAttribute="bottom" constant="49" id="dU2-fJ-SU7"/>
|
||||
<constraint firstItem="ATh-Tf-xRL" firstAttribute="top" secondItem="J5M-pv-UHA" secondAttribute="bottom" constant="8" id="gNc-KC-Q9G"/>
|
||||
<constraint firstItem="unc-nO-WPH" firstAttribute="trailing" secondItem="J5M-pv-UHA" secondAttribute="trailing" constant="66" id="h4a-vk-mBB"/>
|
||||
<constraint firstAttribute="centerX" secondItem="nFR-to-qQs" secondAttribute="centerX" id="h9S-ek-7bf"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<toolbarItems/>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="HostPicker" destination="nFR-to-qQs" id="lTE-tT-lYY"/>
|
||||
<outlet property="StreamConfigs" destination="0xb-lk-PHK" id="VxB-Ur-PNy"/>
|
||||
<outlet property="hostTextField" destination="unc-nO-WPH" id="MbD-KQ-MoG"/>
|
||||
<segue destination="mI3-9F-XwU" kind="modal" identifier="createStreamFrame" id="cPc-eP-un1"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HNf-lX-GEO" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="131" y="0.0"/>
|
||||
</scene>
|
||||
<!--Stream Frame View Controller-->
|
||||
<scene sceneID="5Eb-q2-vjt">
|
||||
<objects>
|
||||
<viewController id="mI3-9F-XwU" customClass="StreamFrameViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="DRq-YB-9Rh"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="KH1-hM-RYW"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="eir-e9-IPE" customClass="StreamView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="0vm-Iv-K4b">
|
||||
<rect key="frame" x="150" y="274" width="20" height="20"/>
|
||||
</activityIndicatorView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Stage" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2HK-Z5-4Ch">
|
||||
<rect key="frame" x="138" y="302" width="45" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="centerX" secondItem="2HK-Z5-4Ch" secondAttribute="centerX" constant="-0.5" id="Blw-Hy-66z"/>
|
||||
<constraint firstAttribute="centerX" secondItem="0vm-Iv-K4b" secondAttribute="centerX" id="mCQ-CP-Yik"/>
|
||||
<constraint firstAttribute="centerY" secondItem="0vm-Iv-K4b" secondAttribute="centerY" id="t8e-qp-g1j"/>
|
||||
<constraint firstItem="2HK-Z5-4Ch" firstAttribute="top" secondItem="0vm-Iv-K4b" secondAttribute="bottom" constant="8" id="tta-Uo-bBO"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="spinner" destination="0vm-Iv-K4b" id="Lif-zG-6Jh"/>
|
||||
<outlet property="stageLabel" destination="2HK-Z5-4Ch" id="1bI-Ig-OpD"/>
|
||||
<segue destination="dgh-JZ-Q7z" kind="modal" identifier="returnToMainFrame" id="djB-MF-L5D"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="pqv-jd-33O" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="584" y="0.0"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
||||
@@ -1,33 +1,12 @@
|
||||
# Moonlight PC
|
||||
#Limelight iOS
|
||||
|
||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
[Limelight](https://github.com/limelight-stream) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built for iOS. Limelight iOS will allow you to stream your full collection of Steam games from
|
||||
your powerful desktop computer to your iOS Device.
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
This is in development and is not considered stable.
|
||||
|
||||
This client is currently still in development (pre-alpha), but the streaming performance is already much better than the older Moonlight Chrome client on many machines. Test releases may be available for your OS on the [Releases page](https://github.com/moonlight-stream/moonlight-qt/releases).
|
||||
We understand that Limelight users are eager for the opportunity to stream their Steam collection to their iOS devices. Limelight for iOS is in the testing stage and a preliminary version will soon be available for download.
|
||||
|
||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
In the meantime, check out the existing versions for [Android](https://github.com/limelight-stream/limelight-android) and [PC](https://github.com/limelight-stream/limelight-pc). Also, a client for [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) is in the works and actively being developed.
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS on high refresh rate monitors
|
||||
- Supports streaming at 720p, 1080p, 1440p, or 4K
|
||||
- 5.1 surround sound audio
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Keyboard and mouse support
|
||||
- Gamepad support with SDL gamepad mappings
|
||||
|
||||
## Building
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download (select MSVC toolchain on Windows)
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
3. Open the project in Qt Creator or build from qmake on the command line
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
|
||||
Check out our [website](http://moonlight-stream.com) for project links and information.
|
||||
Please be patient as we work to bring Limelight to more platforms.
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Moonlight</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>moonlight</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.moonlight-stream.Moonlight</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.0.5</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,192 +0,0 @@
|
||||
QT += core quick network quickcontrols2 svg
|
||||
CONFIG += c++11
|
||||
|
||||
unix:!macx {
|
||||
TARGET = moonlight
|
||||
} else {
|
||||
# On macOS, this is the name displayed in the global menu bar
|
||||
TARGET = Moonlight
|
||||
}
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
TEMPLATE = app
|
||||
|
||||
# The following define makes your compiler emit warnings if you use
|
||||
# any feature of Qt which has been marked as deprecated (the exact warnings
|
||||
# depend on your compiler). Please consult the documentation of the
|
||||
# deprecated API in order to know how to port your code away from it.
|
||||
DEFINES += QT_DEPRECATED_WARNINGS
|
||||
|
||||
# You can also make your code fail to compile if you use deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
# You can also select to disable deprecated APIs only up to a certain version of Qt.
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
win32 {
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||
}
|
||||
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 opus
|
||||
LIBS += -ldl
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
|
||||
packagesExist(libva) {
|
||||
CONFIG += libva
|
||||
}
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
macx {
|
||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
main.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
backend/nvhttp.cpp \
|
||||
backend/nvpairingmanager.cpp \
|
||||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
streaming/session.cpp \
|
||||
streaming/audio.cpp \
|
||||
gui/computermodel.cpp \
|
||||
gui/appmodel.cpp
|
||||
|
||||
HEADERS += \
|
||||
utils.h \
|
||||
backend/identitymanager.h \
|
||||
backend/nvhttp.h \
|
||||
backend/nvpairingmanager.h \
|
||||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.hpp \
|
||||
streaming/session.hpp \
|
||||
gui/computermodel.h \
|
||||
gui/appmodel.h \
|
||||
streaming/video/decoder.h
|
||||
|
||||
# Platform-specific renderers and decoders
|
||||
ffmpeg {
|
||||
message(FFmpeg decoder selected)
|
||||
|
||||
DEFINES += HAVE_FFMPEG
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg.cpp \
|
||||
streaming/video/ffmpeg-renderers/sdl.cpp
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg.h \
|
||||
streaming/video/ffmpeg-renderers/renderer.h
|
||||
}
|
||||
libva {
|
||||
message(VAAPI renderer selected)
|
||||
|
||||
DEFINES += HAVE_LIBVA
|
||||
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
|
||||
}
|
||||
config_SLVideo {
|
||||
message(SLVideo decoder/renderer selected)
|
||||
|
||||
DEFINES += HAVE_SLVIDEO
|
||||
LIBS += -lSLVideo
|
||||
SOURCES += streaming/video/sl.cpp
|
||||
HEADERS += streaming/video/sl.h
|
||||
}
|
||||
win32 {
|
||||
message(DXVA2 renderer selected)
|
||||
|
||||
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
|
||||
}
|
||||
macx {
|
||||
message(VideoToolbox renderer selected)
|
||||
|
||||
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
|
||||
HEADERS += streaming/video/ffmpeg-renderers/vt.h
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
# Additional import path used to resolve QML modules just for Qt Quick Designer
|
||||
QML_DESIGNER_IMPORT_PATH =
|
||||
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../moonlight-common-c/release/ -lmoonlight-common-c
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../moonlight-common-c/debug/ -lmoonlight-common-c
|
||||
else:unix: LIBS += -L$$OUT_PWD/../moonlight-common-c/ -lmoonlight-common-c
|
||||
|
||||
INCLUDEPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
||||
DEPENDPATH += $$PWD/../moonlight-common-c/moonlight-common-c/src
|
||||
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/release/ -lqmdnsengine
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../qmdnsengine/debug/ -lqmdnsengine
|
||||
else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||
|
||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
|
||||
unix:!macx: {
|
||||
isEmpty(PREFIX) {
|
||||
PREFIX = /usr/local
|
||||
}
|
||||
isEmpty(BINDIR) {
|
||||
BINDIR = bin
|
||||
}
|
||||
|
||||
target.path = $$PREFIX/$$BINDIR/
|
||||
|
||||
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
|
||||
desktop.path = $$PREFIX/share/applications/
|
||||
|
||||
icons.files = res/moonlight.svg
|
||||
icons.path = $$PREFIX/share/icons/hicolor/scalable/apps/
|
||||
|
||||
appdata.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||
appdata.path = $$PREFIX/share/metainfo/
|
||||
|
||||
INSTALLS += target desktop icons appdata
|
||||
}
|
||||
win32 {
|
||||
RC_ICONS = moonlight.ico
|
||||
}
|
||||
macx {
|
||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||
}
|
||||
|
||||
VERSION = 0.0.5
|
||||
@@ -1,78 +0,0 @@
|
||||
#include "boxartmanager.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
#include <QImageReader>
|
||||
#include <QImageWriter>
|
||||
|
||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_BoxArtDir(
|
||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart")
|
||||
{
|
||||
if (!m_BoxArtDir.exists()) {
|
||||
m_BoxArtDir.mkpath(".");
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId)
|
||||
{
|
||||
QDir dir = m_BoxArtDir;
|
||||
|
||||
// Create the cache directory if it did not already exist
|
||||
if (!dir.exists(computer->uuid)) {
|
||||
dir.mkdir(computer->uuid);
|
||||
}
|
||||
|
||||
// Change to this computer's box art cache folder
|
||||
dir.cd(computer->uuid);
|
||||
|
||||
// Try to open the cached file
|
||||
return dir.filePath(QString::number(appId) + ".png");
|
||||
}
|
||||
|
||||
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||
{
|
||||
// Try to open the cached file
|
||||
QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
|
||||
if (QFile::exists(cacheFilePath)) {
|
||||
return QUrl::fromLocalFile(cacheFilePath);
|
||||
}
|
||||
|
||||
// If we get here, we need to fetch asynchronously.
|
||||
// Kick off a worker on our thread pool to do just that.
|
||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||
QThreadPool::globalInstance()->start(netLoadTask);
|
||||
|
||||
// Return the placeholder then we can notify the caller
|
||||
// later when the real image is ready.
|
||||
return QUrl("qrc:/res/no_app_image.png");
|
||||
}
|
||||
|
||||
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
||||
{
|
||||
if (image.isEmpty()) {
|
||||
image = QUrl("qrc:/res/no_app_image.png");
|
||||
}
|
||||
emit boxArtLoadComplete(computer, app, image);
|
||||
}
|
||||
|
||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
{
|
||||
NvHTTP http(computer->activeAddress);
|
||||
|
||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||
QImage image;
|
||||
try {
|
||||
image = http.getBoxArt(appId);
|
||||
} catch (...) {}
|
||||
|
||||
// Cache the box art on disk if it loaded
|
||||
if (!image.isNull()) {
|
||||
if (image.save(cachePath)) {
|
||||
return QUrl::fromLocalFile(cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
return QUrl();
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "computermanager.h"
|
||||
#include <QDir>
|
||||
#include <QImage>
|
||||
#include <QThreadPool>
|
||||
#include <QRunnable>
|
||||
|
||||
class BoxArtManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class NetworkBoxArtLoadTask;
|
||||
|
||||
public:
|
||||
explicit BoxArtManager(QObject *parent = nullptr);
|
||||
|
||||
QUrl
|
||||
loadBoxArt(NvComputer* computer, NvApp& app);
|
||||
|
||||
signals:
|
||||
void
|
||||
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
public slots:
|
||||
|
||||
private slots:
|
||||
void
|
||||
handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
private:
|
||||
QUrl
|
||||
loadBoxArtFromNetwork(NvComputer* computer, int appId);
|
||||
|
||||
QString
|
||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||
|
||||
QDir m_BoxArtDir;
|
||||
};
|
||||
|
||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
|
||||
: m_Bam(boxArtManager),
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||
}
|
||||
|
||||
signals:
|
||||
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
if (image.isEmpty()) {
|
||||
// Give it another shot if it fails once
|
||||
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
}
|
||||
emit boxArtFetchCompleted(m_Computer, m_App, image);
|
||||
}
|
||||
|
||||
BoxArtManager* m_Bam;
|
||||
NvComputer* m_Computer;
|
||||
NvApp m_App;
|
||||
};
|
||||
@@ -1,539 +0,0 @@
|
||||
#include "computermanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QThreadPool>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
#define SER_NAME "hostname"
|
||||
#define SER_UUID "uuid"
|
||||
#define SER_MAC "mac"
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_APPLIST "apps"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
this->name = settings.value(SER_NAME).toString();
|
||||
this->uuid = settings.value(SER_UUID).toString();
|
||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
NvApp app;
|
||||
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
app.name = settings.value(SER_APPNAME).toString();
|
||||
app.id = settings.value(SER_APPID).toInt();
|
||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||
|
||||
this->appList.append(app);
|
||||
}
|
||||
settings.endArray();
|
||||
sortAppList();
|
||||
|
||||
this->activeAddress = nullptr;
|
||||
this->currentGameId = 0;
|
||||
this->pairState = PS_UNKNOWN;
|
||||
this->state = CS_UNKNOWN;
|
||||
this->gfeVersion = nullptr;
|
||||
this->appVersion = nullptr;
|
||||
this->maxLumaPixelsHEVC = 0;
|
||||
this->serverCodecModeSupport = 0;
|
||||
this->pendingQuit = false;
|
||||
}
|
||||
|
||||
void
|
||||
NvComputer::serialize(QSettings& settings)
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
settings.setValue(SER_NAME, name);
|
||||
settings.setValue(SER_UUID, uuid);
|
||||
settings.setValue(SER_MAC, macAddress);
|
||||
settings.setValue(SER_LOCALADDR, localAddress);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
settings.setValue(SER_APPNAME, appList[i].name);
|
||||
settings.setValue(SER_APPID, appList[i].id);
|
||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
void NvComputer::sortAppList()
|
||||
{
|
||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||
return app1.name.toLower() < app2.name.toLower();
|
||||
});
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
{
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isNull()) {
|
||||
this->name = "UNKNOWN";
|
||||
}
|
||||
|
||||
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||
if (!codecSupport.isNull()) {
|
||||
this->serverCodecModeSupport = codecSupport.toInt();
|
||||
}
|
||||
else {
|
||||
this->serverCodecModeSupport = 0;
|
||||
}
|
||||
|
||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
if (!maxLumaPixelsHEVC.isNull()) {
|
||||
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
||||
}
|
||||
else {
|
||||
this->maxLumaPixelsHEVC = 0;
|
||||
}
|
||||
|
||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||
return mode1.width * mode1.height * mode1.refreshRate <
|
||||
mode2.width * mode2.height * mode2.refreshRate;
|
||||
});
|
||||
|
||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||
this->activeAddress = address;
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
this->pendingQuit = false;
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
{
|
||||
if (state == NvComputer::CS_ONLINE) {
|
||||
qWarning() << name << "is already online";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (macAddress.isNull()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, // Ports opened by GFE
|
||||
};
|
||||
|
||||
// Create the WoL payload
|
||||
QByteArray wolPayload;
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.count() == 102);
|
||||
|
||||
// Add the addresses that we know this host to be
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
QVector<QString> addressList = uniqueAddresses();
|
||||
addressList.append("255.255.255.255");
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (QString& addressString : addressList) {
|
||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||
|
||||
// Try all IP addresses that this string resolves to
|
||||
for (QHostAddress& address : hostInfo.addresses()) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// Bind to any address on the correct protocol
|
||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
||||
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
QVector<QString> NvComputer::uniqueAddresses()
|
||||
{
|
||||
QVector<QString> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
uniqueAddressList.append(activeAddress);
|
||||
uniqueAddressList.append(localAddress);
|
||||
uniqueAddressList.append(remoteAddress);
|
||||
uniqueAddressList.append(manualAddress);
|
||||
|
||||
// Prune duplicates (always giving precedence to the first)
|
||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||
if (uniqueAddressList[i].isEmpty() || uniqueAddressList[i].isNull()) {
|
||||
uniqueAddressList.remove(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
|
||||
if (uniqueAddressList[i] == uniqueAddressList[j]) {
|
||||
// Always remove the later occurrence
|
||||
uniqueAddressList.remove(j);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must have at least 1 address
|
||||
Q_ASSERT(!uniqueAddressList.isEmpty());
|
||||
|
||||
return uniqueAddressList;
|
||||
}
|
||||
|
||||
bool NvComputer::update(NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
// Lock us for write and them for read
|
||||
QWriteLocker thisLock(&this->lock);
|
||||
QReadLocker thatLock(&that.lock);
|
||||
|
||||
// UUID may not change or we're talking to a new PC
|
||||
Q_ASSERT(this->uuid == that.uuid);
|
||||
|
||||
#define ASSIGN_IF_CHANGED(field) \
|
||||
if (this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
||||
if (!that.field.isEmpty() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
ASSIGN_IF_CHANGED(currentGameId);
|
||||
ASSIGN_IF_CHANGED(activeAddress);
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
return changed;
|
||||
}
|
||||
|
||||
ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_PollingRef(0),
|
||||
m_MdnsBrowser(nullptr)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// Inflate our hosts from QSettings
|
||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
||||
for (int i = 0; i < hosts; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
NvComputer* computer = new NvComputer(settings);
|
||||
m_KnownHosts[computer->uuid] = computer;
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
// Delete machines that haven't been resolved yet
|
||||
while (!m_PendingResolution.isEmpty()) {
|
||||
MdnsPendingComputer* computer = m_PendingResolution.first();
|
||||
delete computer;
|
||||
m_PendingResolution.removeFirst();
|
||||
}
|
||||
|
||||
// Delete the browser to stop discovery
|
||||
delete m_MdnsBrowser;
|
||||
m_MdnsBrowser = nullptr;
|
||||
|
||||
{
|
||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
||||
|
||||
// Interrupt all polling threads
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
i.value()->requestInterruption();
|
||||
}
|
||||
|
||||
// Wait for all threads to terminate before destroying
|
||||
// the NvComputer objects.
|
||||
i.toFront();
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QThread* thread = i.value();
|
||||
thread->wait();
|
||||
delete thread;
|
||||
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||
|
||||
// Destroy all NvComputer objects
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
delete i.value();
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::saveHosts()
|
||||
{
|
||||
QSettings settings;
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void ComputerManager::startPolling()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
if (++m_PollingRef > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start an MDNS query for GameStream hosts
|
||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||
this, [this](const QMdnsEngine::Service& service) {
|
||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
||||
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
||||
m_PendingResolution.append(pendingComputer);
|
||||
});
|
||||
|
||||
// Start polling threads for each known host
|
||||
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
startPollingComputer(i.value());
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
const QHostAddress& address)
|
||||
{
|
||||
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
|
||||
|
||||
addNewHost(address.toString(), true);
|
||||
|
||||
m_PendingResolution.removeOne(computer);
|
||||
computer->deleteLater();
|
||||
}
|
||||
|
||||
QVector<NvComputer*> ComputerManager::getComputers()
|
||||
{
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||
}
|
||||
|
||||
class DeferredHostDeletionTask : public QRunnable
|
||||
{
|
||||
public:
|
||||
DeferredHostDeletionTask(ComputerManager* cm, NvComputer* computer)
|
||||
: m_Computer(computer),
|
||||
m_ComputerManager(cm) {}
|
||||
|
||||
void run()
|
||||
{
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
|
||||
QThread* pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
||||
if (pollingThread != nullptr) {
|
||||
pollingThread->requestInterruption();
|
||||
|
||||
// We must wait here because we're going to delete computer
|
||||
// and we can't do that out from underneath the poller.
|
||||
pollingThread->wait();
|
||||
|
||||
// The thread is safe to delete now
|
||||
Q_ASSERT(pollingThread->isFinished());
|
||||
delete pollingThread;
|
||||
}
|
||||
|
||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||
|
||||
delete m_Computer;
|
||||
}
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
ComputerManager* m_ComputerManager;
|
||||
};
|
||||
|
||||
void ComputerManager::deleteHost(NvComputer* computer)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
// UI while waiting for the polling thread to die
|
||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||
}
|
||||
|
||||
void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
// UI while waiting for pairing to complete
|
||||
PendingPairingTask* pairing = new PendingPairingTask(this, computer, pin);
|
||||
QThreadPool::globalInstance()->start(pairing);
|
||||
}
|
||||
|
||||
void ComputerManager::quitRunningApp(NvComputer* computer)
|
||||
{
|
||||
QWriteLocker lock(&computer->lock);
|
||||
computer->pendingQuit = true;
|
||||
|
||||
PendingQuitTask* quit = new PendingQuitTask(this, computer);
|
||||
QThreadPool::globalInstance()->start(quit);
|
||||
}
|
||||
|
||||
void ComputerManager::stopPollingAsync()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
Q_ASSERT(m_PollingRef > 0);
|
||||
if (--m_PollingRef > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete machines that haven't been resolved yet
|
||||
while (!m_PendingResolution.isEmpty()) {
|
||||
MdnsPendingComputer* computer = m_PendingResolution.first();
|
||||
computer->deleteLater();
|
||||
m_PendingResolution.removeFirst();
|
||||
}
|
||||
|
||||
// Delete the browser to stop discovery
|
||||
delete m_MdnsBrowser;
|
||||
m_MdnsBrowser = nullptr;
|
||||
|
||||
// Interrupt all threads, but don't wait for them to terminate
|
||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QThread* thread = i.value();
|
||||
|
||||
// Let this thread delete itself when it's finished
|
||||
connect(thread, SIGNAL(finished()),
|
||||
thread, SLOT(deleteLater()));
|
||||
|
||||
// The threads will delete themselves when they terminate,
|
||||
// but we remove them from the polling threads map here.
|
||||
i.value()->requestInterruption();
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::addNewHost(QString address, bool mdns)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
// UI while waiting for serverinfo query to complete
|
||||
PendingAddTask* addTask = new PendingAddTask(this, address, mdns);
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
void
|
||||
ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
emit computerStateChanged(computer);
|
||||
|
||||
if (computer->pendingQuit && computer->currentGameId == 0) {
|
||||
computer->pendingQuit = false;
|
||||
emit quitAppCompleted(QVariant());
|
||||
}
|
||||
|
||||
// Save updated hosts to QSettings
|
||||
saveHosts();
|
||||
}
|
||||
|
||||
// Must hold m_Lock for write
|
||||
void
|
||||
ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
{
|
||||
if (m_PollingRef == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_PollThreads.contains(computer->uuid)) {
|
||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
||||
return;
|
||||
}
|
||||
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
m_PollThreads[computer->uuid] = thread;
|
||||
thread->start();
|
||||
}
|
||||
|
||||
@@ -1,477 +0,0 @@
|
||||
#pragma once
|
||||
#include "nvhttp.h"
|
||||
#include "nvpairingmanager.h"
|
||||
|
||||
#include <qmdnsengine/server.h>
|
||||
#include <qmdnsengine/cache.h>
|
||||
#include <qmdnsengine/browser.h>
|
||||
#include <qmdnsengine/service.h>
|
||||
#include <qmdnsengine/resolver.h>
|
||||
|
||||
#include <QThread>
|
||||
#include <QReadWriteLock>
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
|
||||
class NvComputer
|
||||
{
|
||||
friend class PcMonitorThread;
|
||||
friend class ComputerManager;
|
||||
friend class PendingQuitTask;
|
||||
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
|
||||
bool
|
||||
wake();
|
||||
|
||||
QVector<QString>
|
||||
uniqueAddresses();
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
|
||||
enum PairState
|
||||
{
|
||||
PS_UNKNOWN,
|
||||
PS_PAIRED,
|
||||
PS_NOT_PAIRED
|
||||
};
|
||||
|
||||
enum ComputerState
|
||||
{
|
||||
CS_UNKNOWN,
|
||||
CS_ONLINE,
|
||||
CS_OFFLINE
|
||||
};
|
||||
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
QString activeAddress;
|
||||
int currentGameId;
|
||||
QString gfeVersion;
|
||||
QString appVersion;
|
||||
QVector<NvDisplayMode> displayModes;
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
QString uuid;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
};
|
||||
|
||||
// FIXME: MOC isn't finding Q_OBJECT properly when this is confined
|
||||
// to computermanager.cpp as it should be.
|
||||
class PcMonitorThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
#define TRIES_BEFORE_OFFLINING 2
|
||||
#define POLLS_PER_APPLIST_FETCH 10
|
||||
|
||||
public:
|
||||
PcMonitorThread(NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
setObjectName("Polling thread for " + computer->name);
|
||||
}
|
||||
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo);
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
||||
return false;
|
||||
}
|
||||
|
||||
changed = m_Computer->update(newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
try {
|
||||
appList = http.getAppList();
|
||||
if (appList.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
if (m_Computer->appList != appList) {
|
||||
m_Computer->appList = appList;
|
||||
m_Computer->sortAppList();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Always fetch the applist the first time
|
||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||
while (!isInterruptionRequested()) {
|
||||
bool stateChanged = false;
|
||||
bool online = false;
|
||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||
if (isInterruptionRequested()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
online = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we failed after all retry attempts
|
||||
// Note: we don't need to acquire the read lock here,
|
||||
// because we're on the writing thread.
|
||||
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||
pollsSinceLastAppListFetch++;
|
||||
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
||||
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
||||
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
||||
if (updateAppList(stateChanged)) {
|
||||
pollsSinceLastAppListFetch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
// Tell anyone listening that we've changed state
|
||||
emit computerStateChanged(m_Computer);
|
||||
}
|
||||
|
||||
// Wait a bit to poll again
|
||||
QThread::sleep(3);
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||
QMdnsEngine::Cache* cache,
|
||||
const QMdnsEngine::Service& service)
|
||||
: m_Hostname(service.hostname()),
|
||||
m_Resolver(server, m_Hostname, cache)
|
||||
{
|
||||
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
|
||||
this, SLOT(handleResolved(QHostAddress)));
|
||||
}
|
||||
|
||||
QString hostname()
|
||||
{
|
||||
return m_Hostname;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void handleResolved(const QHostAddress& address)
|
||||
{
|
||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
m_Resolver.disconnect();
|
||||
emit resolvedv4(this, address);
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
|
||||
|
||||
private:
|
||||
QByteArray m_Hostname;
|
||||
QMdnsEngine::Resolver m_Resolver;
|
||||
};
|
||||
|
||||
class ComputerManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class DeferredHostDeletionTask;
|
||||
friend class PendingAddTask;
|
||||
|
||||
public:
|
||||
explicit ComputerManager(QObject *parent = nullptr);
|
||||
|
||||
virtual ~ComputerManager();
|
||||
|
||||
Q_INVOKABLE void startPolling();
|
||||
|
||||
Q_INVOKABLE void stopPollingAsync();
|
||||
|
||||
Q_INVOKABLE void addNewHost(QString address, bool mdns);
|
||||
|
||||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
void quitRunningApp(NvComputer* computer);
|
||||
|
||||
QVector<NvComputer*> getComputers();
|
||||
|
||||
// computer is deleted inside this call
|
||||
void deleteHost(NvComputer* computer);
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
void pairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
void computerAddCompleted(QVariant success);
|
||||
|
||||
void quitAppCompleted(QVariant error);
|
||||
|
||||
private slots:
|
||||
void handleComputerStateChanged(NvComputer* computer);
|
||||
|
||||
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
|
||||
|
||||
private:
|
||||
void saveHosts();
|
||||
|
||||
void startPollingComputer(NvComputer* computer);
|
||||
|
||||
int m_PollingRef;
|
||||
QReadWriteLock m_Lock;
|
||||
QMap<QString, NvComputer*> m_KnownHosts;
|
||||
QMap<QString, QThread*> m_PollThreads;
|
||||
QMdnsEngine::Server m_MdnsServer;
|
||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
};
|
||||
|
||||
class PendingPairingTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||
: m_Computer(computer),
|
||||
m_Pin(pin)
|
||||
{
|
||||
connect(this, &PendingPairingTask::pairingCompleted,
|
||||
computerManager, &ComputerManager::pairingCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void pairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
||||
break;
|
||||
case NvPairingManager::PairState::FAILED:
|
||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
||||
break;
|
||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
QString m_Pin;
|
||||
};
|
||||
|
||||
class PendingQuitTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
connect(this, &PendingQuitTask::quitAppFailed,
|
||||
computerManager, &ComputerManager::quitAppCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void quitAppFailed(QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
http.quitApp();
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
m_Computer->pendingQuit = false;
|
||||
}
|
||||
if (e.getStatusCode() == 599) {
|
||||
// 599 is a special code we make a custom message for
|
||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||
}
|
||||
else {
|
||||
emit quitAppFailed(e.toQString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
class PendingAddTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_Mdns(mdns)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
computerManager, &ComputerManager::computerAddCompleted);
|
||||
connect(this, &PendingAddTask::computerStateChanged,
|
||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (m_Mdns) {
|
||||
newComputer->localAddress = m_Address;
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
bool m_Mdns;
|
||||
};
|
||||
@@ -1,220 +0,0 @@
|
||||
#include "identitymanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#define SER_UNIQUEID "uniqueid"
|
||||
#define SER_CERT "certificate"
|
||||
#define SER_KEY "key"
|
||||
|
||||
IdentityManager* IdentityManager::s_Im = nullptr;
|
||||
|
||||
IdentityManager*
|
||||
IdentityManager::get()
|
||||
{
|
||||
// This will always be called first on the main thread,
|
||||
// so it's safe to initialize without locks.
|
||||
if (s_Im == nullptr) {
|
||||
s_Im = new IdentityManager();
|
||||
}
|
||||
|
||||
return s_Im;
|
||||
}
|
||||
|
||||
void IdentityManager::createCredentials(QSettings& settings)
|
||||
{
|
||||
X509* cert = X509_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||
|
||||
EVP_PKEY* pk = EVP_PKEY_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||
|
||||
BIGNUM* bne = BN_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(bne);
|
||||
|
||||
RSA* rsa = RSA_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(rsa);
|
||||
|
||||
BN_set_word(bne, RSA_F4);
|
||||
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
|
||||
|
||||
EVP_PKEY_assign_RSA(pk, rsa);
|
||||
|
||||
X509_set_version(cert, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
X509_gmtime_adj(X509_get_notBefore(cert), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
#else
|
||||
ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
|
||||
THROW_BAD_ALLOC_IF_NULL(before);
|
||||
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
|
||||
THROW_BAD_ALLOC_IF_NULL(after);
|
||||
|
||||
X509_gmtime_adj(before, 0);
|
||||
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||
|
||||
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,
|
||||
reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")),
|
||||
-1, -1, 0);
|
||||
X509_set_issuer_name(cert, name);
|
||||
|
||||
X509_sign(cert, pk, EVP_sha1());
|
||||
|
||||
BIO* biokey = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(biokey);
|
||||
PEM_write_bio_PrivateKey(biokey, pk, NULL, NULL, 0, NULL, NULL);
|
||||
|
||||
BIO* biocert = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(biocert);
|
||||
PEM_write_bio_X509(biocert, cert);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(biokey, &mem);
|
||||
m_CachedPrivateKey = QByteArray(mem->data, (int)mem->length);
|
||||
|
||||
BIO_get_mem_ptr(biocert, &mem);
|
||||
m_CachedPemCert = QByteArray(mem->data, (int)mem->length);
|
||||
|
||||
X509_free(cert);
|
||||
EVP_PKEY_free(pk);
|
||||
BN_free(bne);
|
||||
BIO_free(biokey);
|
||||
BIO_free(biocert);
|
||||
|
||||
settings.setValue(SER_CERT, m_CachedPemCert);
|
||||
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
||||
|
||||
qInfo() << "Wrote new identity credentials to settings";
|
||||
}
|
||||
|
||||
IdentityManager::IdentityManager()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
m_CachedPemCert = settings.value(SER_CERT).toByteArray();
|
||||
m_CachedPrivateKey = settings.value(SER_KEY).toByteArray();
|
||||
|
||||
if (m_CachedPemCert.isEmpty() || m_CachedPrivateKey.isEmpty()) {
|
||||
qInfo() << "No existing credentials found";
|
||||
createCredentials(settings);
|
||||
}
|
||||
else if (getSslCertificate().isNull()) {
|
||||
qWarning() << "Certificate is unreadable";
|
||||
createCredentials(settings);
|
||||
}
|
||||
else if (getSslKey().isNull()) {
|
||||
qWarning() << "Private key is unreadable";
|
||||
createCredentials(settings);
|
||||
}
|
||||
|
||||
// We should have valid credentials now. If not, we're screwed
|
||||
if (getSslCertificate().isNull()) {
|
||||
qFatal("Newly generated certificate is unreadable");
|
||||
}
|
||||
if (getSslKey().isNull()) {
|
||||
qFatal("Newly generated private key is unreadable");
|
||||
}
|
||||
}
|
||||
|
||||
QSslCertificate
|
||||
IdentityManager::getSslCertificate()
|
||||
{
|
||||
if (m_CachedSslCert.isNull()) {
|
||||
m_CachedSslCert = QSslCertificate(m_CachedPemCert);
|
||||
}
|
||||
return m_CachedSslCert;
|
||||
}
|
||||
|
||||
QSslKey
|
||||
IdentityManager::getSslKey()
|
||||
{
|
||||
if (m_CachedSslKey.isNull()) {
|
||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free(bio);
|
||||
|
||||
bio = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
// We must write out our PEM in the old PKCS1 format for SecureTransport
|
||||
// on macOS/iOS to be able to read it.
|
||||
#ifdef Q_OS_DARWIN
|
||||
PEM_write_bio_PrivateKey_traditional(bio, pk, nullptr, nullptr, 0, nullptr, 0);
|
||||
#else
|
||||
PEM_write_bio_PrivateKey(bio, pk, nullptr, nullptr, 0, nullptr, 0);
|
||||
#endif
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
|
||||
|
||||
BIO_free(bio);
|
||||
EVP_PKEY_free(pk);
|
||||
}
|
||||
return m_CachedSslKey;
|
||||
}
|
||||
|
||||
QSslConfiguration
|
||||
IdentityManager::getSslConfig()
|
||||
{
|
||||
QSslConfiguration sslConfig(QSslConfiguration::defaultConfiguration());
|
||||
sslConfig.setLocalCertificate(getSslCertificate());
|
||||
sslConfig.setPrivateKey(getSslKey());
|
||||
return sslConfig;
|
||||
}
|
||||
|
||||
QString
|
||||
IdentityManager::getUniqueId()
|
||||
{
|
||||
if (m_CachedUniqueId.isNull()) {
|
||||
QSettings settings;
|
||||
|
||||
// Load the unique ID from settings
|
||||
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
||||
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
||||
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
|
||||
}
|
||||
else {
|
||||
// Generate a new unique ID in base 16
|
||||
uint64_t uid;
|
||||
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
|
||||
m_CachedUniqueId = QString::number(uid, 16);
|
||||
|
||||
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
|
||||
|
||||
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
|
||||
}
|
||||
}
|
||||
return m_CachedUniqueId;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
IdentityManager::getCertificate()
|
||||
{
|
||||
return m_CachedPemCert;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
IdentityManager::getPrivateKey()
|
||||
{
|
||||
return m_CachedPrivateKey;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSslConfiguration>
|
||||
#include <QSslCertificate>
|
||||
#include <QSslKey>
|
||||
#include <QSettings>
|
||||
|
||||
class IdentityManager
|
||||
{
|
||||
public:
|
||||
QString
|
||||
getUniqueId();
|
||||
|
||||
QByteArray
|
||||
getCertificate();
|
||||
|
||||
QByteArray
|
||||
getPrivateKey();
|
||||
|
||||
QSslConfiguration
|
||||
getSslConfig();
|
||||
|
||||
static
|
||||
IdentityManager*
|
||||
get();
|
||||
|
||||
private:
|
||||
IdentityManager();
|
||||
|
||||
QSslCertificate
|
||||
getSslCertificate();
|
||||
|
||||
QSslKey
|
||||
getSslKey();
|
||||
|
||||
void
|
||||
createCredentials(QSettings& settings);
|
||||
|
||||
// Initialized in constructor
|
||||
QByteArray m_CachedPrivateKey;
|
||||
QByteArray m_CachedPemCert;
|
||||
|
||||
// Lazy initialized
|
||||
QString m_CachedUniqueId;
|
||||
QSslCertificate m_CachedSslCert;
|
||||
QSslKey m_CachedSslKey;
|
||||
|
||||
static IdentityManager* s_Im;
|
||||
};
|
||||
@@ -1,408 +0,0 @@
|
||||
#include "nvhttp.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QSslKey>
|
||||
#include <QImageReader>
|
||||
#include <QtEndian>
|
||||
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
|
||||
NvHTTP::NvHTTP(QString address) :
|
||||
m_Address(address)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
m_BaseUrlHttps.setHost(address);
|
||||
m_BaseUrlHttp.setPort(47989);
|
||||
m_BaseUrlHttps.setPort(47984);
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
NvHTTP::parseQuad(QString quad)
|
||||
{
|
||||
QStringList parts = quad.split(".");
|
||||
QVector<int> ret;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
NvHTTP::getCurrentGame(QString serverInfo)
|
||||
{
|
||||
// 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.
|
||||
QString serverState = getXmlString(serverInfo, "state");
|
||||
if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY"))
|
||||
{
|
||||
return getXmlString(serverInfo, "currentgame").toInt();
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getServerInfo()
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
try
|
||||
{
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
if (e.getStatusCode() == 401)
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
static QString
|
||||
getSurroundAudioInfoString(int audioConfig)
|
||||
{
|
||||
int channelMask;
|
||||
int channelCount;
|
||||
|
||||
switch (audioConfig)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
channelCount = 2;
|
||||
channelMask = 0x3;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
channelCount = 6;
|
||||
channelMask = 0xFC;
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return QString::number(channelMask << 16 | channelCount);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||
riKeyId = qFromBigEndian(riKeyId);
|
||||
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"launch",
|
||||
"appid="+QString::number(appId)+
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
QString::number(streamConfig->fps)+
|
||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
(streamConfig->enableHdr ?
|
||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||
"")+
|
||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
false);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||
riKeyId = qFromBigEndian(riKeyId);
|
||||
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||
false);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::quitApp()
|
||||
{
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"cancel",
|
||||
nullptr,
|
||||
false);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
if (getCurrentGame(getServerInfo()) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw GfeHttpResponseException(599, "");
|
||||
}
|
||||
}
|
||||
|
||||
QVector<NvDisplayMode>
|
||||
NvHTTP::getDisplayModeList(QString serverInfo)
|
||||
{
|
||||
QXmlStreamReader xmlReader(serverInfo);
|
||||
QVector<NvDisplayMode> modes;
|
||||
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "DisplayMode") {
|
||||
modes.append(NvDisplayMode());
|
||||
}
|
||||
else if (xmlReader.name() == "Width") {
|
||||
modes.last().width = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "Height") {
|
||||
modes.last().height = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "RefreshRate") {
|
||||
modes.last().refreshRate = xmlReader.readElementText().toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modes;
|
||||
}
|
||||
|
||||
QVector<NvApp>
|
||||
NvHTTP::getAppList()
|
||||
{
|
||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||
"applist",
|
||||
nullptr,
|
||||
true);
|
||||
verifyResponseStatus(appxml);
|
||||
|
||||
QXmlStreamReader xmlReader(appxml);
|
||||
QVector<NvApp> apps;
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "App") {
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
Q_ASSERT(false);
|
||||
return QVector<NvApp>();
|
||||
}
|
||||
apps.append(NvApp());
|
||||
}
|
||||
else if (xmlReader.name() == "AppTitle") {
|
||||
apps.last().name = xmlReader.readElementText();
|
||||
}
|
||||
else if (xmlReader.name() == "ID") {
|
||||
apps.last().id = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "IsHdrSupported") {
|
||||
apps.last().hdrSupported = xmlReader.readElementText() == "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::verifyResponseStatus(QString xml)
|
||||
{
|
||||
QXmlStreamReader xmlReader(xml);
|
||||
|
||||
while (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == "root")
|
||||
{
|
||||
int statusCode = xmlReader.attributes().value("status_code").toInt();
|
||||
if (statusCode == 200)
|
||||
{
|
||||
// Successful
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
QString statusMessage = xmlReader.attributes().value("status_message").toString();
|
||||
if (statusCode != 401) {
|
||||
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
|
||||
qWarning() << "Request failed:" << statusCode << statusMessage;
|
||||
}
|
||||
throw GfeHttpResponseException(statusCode, statusMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QImage
|
||||
NvHTTP::getBoxArt(int appId)
|
||||
{
|
||||
QNetworkReply* reply = openConnection(m_BaseUrlHttps,
|
||||
"appasset",
|
||||
"appid="+QString::number(appId)+
|
||||
"&AssetType=2&AssetIdx=0",
|
||||
true);
|
||||
QImage image = QImageReader(reply).read();
|
||||
delete reply;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvHTTP::getXmlStringFromHex(QString xml,
|
||||
QString tagName)
|
||||
{
|
||||
QString str = getXmlString(xml, tagName);
|
||||
if (str == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return QByteArray::fromHex(str.toLatin1());
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getXmlString(QString xml,
|
||||
QString tagName)
|
||||
{
|
||||
QXmlStreamReader xmlReader(xml);
|
||||
|
||||
while (!xmlReader.atEnd())
|
||||
{
|
||||
if (xmlReader.readNext() != QXmlStreamReader::StartElement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (xmlReader.name() == tagName)
|
||||
{
|
||||
return xmlReader.readElementText();
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout)
|
||||
{
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout);
|
||||
QString ret;
|
||||
|
||||
QTextStream stream(reply);
|
||||
stream.setCodec("UTF-8");
|
||||
ret = stream.readAll();
|
||||
delete reply;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
QNetworkReply*
|
||||
NvHTTP::openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout)
|
||||
{
|
||||
// Build a URL for the request
|
||||
QUrl url(baseUrl);
|
||||
url.setPath("/" + command);
|
||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
|
||||
// Ignore self-signed certificate errors (since GFE uses them)
|
||||
reply->ignoreSslErrors();
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
if (enableTimeout)
|
||||
{
|
||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||
}
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
// Abort the request if it timed out
|
||||
if (!reply->isFinished())
|
||||
{
|
||||
qWarning() << "Aborting timed out request for" << url.toString();
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
// We must clear out cached authentication and connections or
|
||||
// GFE will puke next time
|
||||
m_Nam.clearAccessCache();
|
||||
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "identitymanager.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QUrl>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
|
||||
class NvApp
|
||||
{
|
||||
public:
|
||||
bool operator==(const NvApp& other) const
|
||||
{
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
bool isInitialized()
|
||||
{
|
||||
return id != 0 && !name.isNull();
|
||||
}
|
||||
|
||||
int id;
|
||||
QString name;
|
||||
bool hdrSupported;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(NvApp)
|
||||
|
||||
class NvDisplayMode
|
||||
{
|
||||
public:
|
||||
bool operator==(const NvDisplayMode& other) const
|
||||
{
|
||||
return width == other.width &&
|
||||
height == other.height &&
|
||||
refreshRate == other.refreshRate;
|
||||
}
|
||||
|
||||
int width;
|
||||
int height;
|
||||
int refreshRate;
|
||||
};
|
||||
|
||||
class GfeHttpResponseException : public std::exception
|
||||
{
|
||||
public:
|
||||
GfeHttpResponseException(int statusCode, QString message) :
|
||||
m_StatusCode(statusCode),
|
||||
m_StatusMessage(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
const char* what() const throw()
|
||||
{
|
||||
return m_StatusMessage.toLatin1();
|
||||
}
|
||||
|
||||
const char* getStatusMessage() const
|
||||
{
|
||||
return m_StatusMessage.toLatin1();
|
||||
}
|
||||
|
||||
int getStatusCode() const
|
||||
{
|
||||
return m_StatusCode;
|
||||
}
|
||||
|
||||
QString toQString() const
|
||||
{
|
||||
return m_StatusMessage + " (Error " + QString::number(m_StatusCode) + ")";
|
||||
}
|
||||
|
||||
private:
|
||||
int m_StatusCode;
|
||||
QString m_StatusMessage;
|
||||
};
|
||||
|
||||
class NvHTTP
|
||||
{
|
||||
public:
|
||||
explicit NvHTTP(QString address);
|
||||
|
||||
static
|
||||
int
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo();
|
||||
|
||||
static
|
||||
void
|
||||
verifyResponseStatus(QString xml);
|
||||
|
||||
static
|
||||
QString
|
||||
getXmlString(QString xml,
|
||||
QString tagName);
|
||||
|
||||
static
|
||||
QByteArray
|
||||
getXmlStringFromHex(QString xml,
|
||||
QString tagName);
|
||||
|
||||
QString
|
||||
openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout);
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
parseQuad(QString quad);
|
||||
|
||||
void
|
||||
quitApp();
|
||||
|
||||
void
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig);
|
||||
|
||||
void
|
||||
launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask);
|
||||
|
||||
QVector<NvApp>
|
||||
getAppList();
|
||||
|
||||
QImage
|
||||
getBoxArt(int appId);
|
||||
|
||||
static
|
||||
QVector<NvDisplayMode>
|
||||
getDisplayModeList(QString serverInfo);
|
||||
|
||||
QUrl m_BaseUrlHttp;
|
||||
QUrl m_BaseUrlHttps;
|
||||
private:
|
||||
QNetworkReply*
|
||||
openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout);
|
||||
|
||||
QString m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -1,313 +0,0 @@
|
||||
#include "nvpairingmanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
NvPairingManager::NvPairingManager(QString address) :
|
||||
m_Http(address)
|
||||
{
|
||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
m_Cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
if (m_Cert == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Unable to load certificate");
|
||||
}
|
||||
|
||||
QByteArray pk = IdentityManager::get()->getPrivateKey();
|
||||
bio = BIO_new_mem_buf(pk.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
m_PrivateKey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
if (m_PrivateKey == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Unable to load private key");
|
||||
}
|
||||
}
|
||||
|
||||
NvPairingManager::~NvPairingManager()
|
||||
{
|
||||
X509_free(m_Cert);
|
||||
EVP_PKEY_free(m_PrivateKey);
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::generateRandomBytes(int length)
|
||||
{
|
||||
char* data = static_cast<char*>(alloca(length));
|
||||
RAND_bytes(reinterpret_cast<unsigned char*>(data), length);
|
||||
return QByteArray(data, length);
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
||||
{
|
||||
QByteArray ciphertext(plaintext.size(), 0);
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
key);
|
||||
}
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
||||
{
|
||||
QByteArray plaintext(ciphertext.size(), 0);
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
key);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
{
|
||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||
ASN1_BIT_STRING *asnSignature = cert->signature;
|
||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, cert);
|
||||
#endif
|
||||
|
||||
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
|
||||
X509_free(cert);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
bool
|
||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
||||
{
|
||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
BIO_free_all(bio);
|
||||
|
||||
EVP_PKEY* pubKey = X509_get_pubkey(cert);
|
||||
THROW_BAD_ALLOC_IF_NULL(pubKey);
|
||||
|
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(mdctx);
|
||||
|
||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
||||
|
||||
EVP_PKEY_free(pubKey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
X509_free(cert);
|
||||
|
||||
return result > 0;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::signMessage(QByteArray message)
|
||||
{
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
const EVP_MD *md = EVP_get_digestbyname("SHA256");
|
||||
THROW_BAD_ALLOC_IF_NULL(md);
|
||||
|
||||
EVP_DigestInit_ex(ctx, md, NULL);
|
||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
|
||||
|
||||
size_t signatureLength = 0;
|
||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||
|
||||
QByteArray signature((int)signatureLength, 0);
|
||||
EVP_DigestSignFinal(ctx, reinterpret_cast<unsigned char*>(signature.data()), &signatureLength);
|
||||
|
||||
EVP_MD_CTX_destroy(ctx);
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
||||
{
|
||||
return QByteArray().append(salt).append(pin.toLatin1());
|
||||
}
|
||||
|
||||
NvPairingManager::PairState
|
||||
NvPairingManager::pair(QString appVersion, QString pin)
|
||||
{
|
||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||
|
||||
QCryptographicHash::Algorithm hashAlgo;
|
||||
int hashLength;
|
||||
if (serverMajorVersion >= 7)
|
||||
{
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = QCryptographicHash::Sha256;
|
||||
hashLength = 32;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prior to Gen 7 uses SHA-1 hashing
|
||||
hashAlgo = QCryptographicHash::Sha1;
|
||||
hashLength = 20;
|
||||
}
|
||||
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
AES_KEY encKey, decKey;
|
||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||
false);
|
||||
NvHTTP::verifyResponseStatus(getCert);
|
||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #1";
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCert == nullptr)
|
||||
{
|
||||
qCritical() << "Server likely already pairing";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientchallenge=" +
|
||||
encryptedChallenge.toHex(),
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(challengeXml);
|
||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #2";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
||||
QByteArray clientSecretData = generateRandomBytes(16);
|
||||
QByteArray challengeResponse;
|
||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
|
||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
||||
#endif
|
||||
|
||||
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
challengeResponse.append(clientSecretData);
|
||||
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
|
||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||
encryptedChallengeResponseHash.toHex(),
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(respXml);
|
||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #3";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret");
|
||||
QByteArray serverSecret = QByteArray(pairingSecret.data(), 16);
|
||||
QByteArray serverSignature = QByteArray(&pairingSecret.data()[16], 256);
|
||||
|
||||
if (!verifySignature(serverSecret,
|
||||
serverSignature,
|
||||
serverCert))
|
||||
{
|
||||
qCritical() << "MITM detected";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray expectedResponseData;
|
||||
expectedResponseData.append(randomChallenge);
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
||||
expectedResponseData.append(serverSecret);
|
||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||
{
|
||||
qCritical() << "Incorrect PIN";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::PIN_WRONG;
|
||||
}
|
||||
|
||||
QByteArray clientPairingSecret;
|
||||
clientPairingSecret.append(clientSecretData);
|
||||
clientPairingSecret.append(signMessage(clientSecretData));
|
||||
|
||||
QString secretRespXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||
clientPairingSecret.toHex(),
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #4";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||
true);
|
||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #5";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
return PairState::PAIRED;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "identitymanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
class NvPairingManager
|
||||
{
|
||||
public:
|
||||
enum PairState
|
||||
{
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
};
|
||||
|
||||
explicit NvPairingManager(QString address);
|
||||
|
||||
~NvPairingManager();
|
||||
|
||||
PairState
|
||||
pair(QString appVersion, QString pin);
|
||||
|
||||
private:
|
||||
QByteArray
|
||||
generateRandomBytes(int length);
|
||||
|
||||
QByteArray
|
||||
saltPin(QByteArray salt, QString pin);
|
||||
|
||||
QByteArray
|
||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
||||
|
||||
QByteArray
|
||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromPemCert(QByteArray certificate);
|
||||
|
||||
bool
|
||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
||||
|
||||
QByteArray
|
||||
signMessage(QByteArray message);
|
||||
|
||||
NvHTTP m_Http;
|
||||
X509* m_Cert;
|
||||
EVP_PKEY* m_PrivateKey;
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>com.moonlight_stream.Moonlight</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Moonlight</name>
|
||||
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
||||
|
||||
<description>
|
||||
<p>Moonlight can stream games or other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||
<p>GameStream utilizes the NVENC hardware encoder present on NVIDIA GPUs for low-latency streaming at up 4K 60 FPS. Moonlight can utilize VAAPI hardware acceleration for high-performance H.264 and HEVC video decoding with low CPU usage.</p>
|
||||
</description>
|
||||
|
||||
<url type="homepage">https://moonlight-stream.com</url>
|
||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="0.0.5" date="2018-08-01">
|
||||
<description>
|
||||
<p>Changes from 0.0.4:</p>
|
||||
<ul>
|
||||
<li>Fixed a bug causing increased frame drops and input lag</li>
|
||||
<li>Added support for quitting running games</li>
|
||||
<li>Added support for waking sleeping PCs with Wake-on-LAN</li>
|
||||
<li>Increased the FPS limit to 120 FPS</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.0.4a" date="2018-07-30">
|
||||
<description>
|
||||
<p>Initial pre-alpha release for Linux. Please file bugs!</p>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
|
||||
<categories>
|
||||
<category>Game</category>
|
||||
<category>Qt</category>
|
||||
</categories>
|
||||
|
||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||
<update_contact>cameron@moonlight-stream.com</update_contact>
|
||||
|
||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||
|
||||
<provides>
|
||||
<binary>moonlight</binary>
|
||||
</provides>
|
||||
|
||||
<content_rating type="oars-1.0" />
|
||||
</component>
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=Moonlight
|
||||
Comment=Stream games from your NVIDIA GameStream-enabled PC
|
||||
Exec=moonlight
|
||||
Icon=moonlight
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Qt;Game;
|
||||
Keywords=nvidia;gamestream;stream;
|
||||
@@ -1,180 +0,0 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import AppModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
|
||||
GridView {
|
||||
property int computerIndex
|
||||
property AppModel appModel : createModel()
|
||||
|
||||
id: appGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 225; cellHeight: 350;
|
||||
focus: true
|
||||
|
||||
// Cache delegates for 1000px in both directions to improve
|
||||
// scrolling and resizing performance
|
||||
cacheBuffer: 1000
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Start polling when this view is shown
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
else {
|
||||
// Stop polling when this view is not on top
|
||||
ComputerManager.stopPollingAsync()
|
||||
}
|
||||
}
|
||||
|
||||
function createModel()
|
||||
{
|
||||
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
||||
model.initialize(ComputerManager, computerIndex)
|
||||
return model
|
||||
}
|
||||
|
||||
model: appModel
|
||||
|
||||
delegate: Item {
|
||||
width: 200; height: 300;
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter;
|
||||
source: model.boxart
|
||||
sourceSize {
|
||||
width: 150
|
||||
height: 200
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: appNameText
|
||||
text: {
|
||||
if (model.running) {
|
||||
return "<font color=\"green\">Running</font><font color=\"white\"> - </font>" + model.name
|
||||
}
|
||||
else {
|
||||
return model.name
|
||||
}
|
||||
}
|
||||
|
||||
color: "white"
|
||||
|
||||
width: parent.width
|
||||
height: 125
|
||||
anchors.top: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp()
|
||||
{
|
||||
var runningIndex = appModel.getRunningAppIndex()
|
||||
if (runningIndex >= 0 && runningIndex !== index) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.open()
|
||||
return
|
||||
}
|
||||
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = model.name
|
||||
segue.session = appModel.createSessionForApp(index)
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
property string appName : "";
|
||||
property bool segueToStream : false
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = appName
|
||||
if (segueToStream) {
|
||||
// Store the session and app name if we're going to stream after
|
||||
// successfully quitting the old app.
|
||||
segue.nextAppName = model.name
|
||||
segue.nextSession = appModel.createSessionForApp(index)
|
||||
}
|
||||
else {
|
||||
segue.nextAppName = null
|
||||
segue.nextSession = null
|
||||
}
|
||||
|
||||
stackView.push(segue)
|
||||
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
appModel.quitRunningApp()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Nothing is running or this app is running
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
else {
|
||||
// Right click
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: appContextMenu
|
||||
MenuItem {
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: {
|
||||
appContextMenu.close()
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem {
|
||||
text: "Quit Game"
|
||||
onTriggered: {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = false
|
||||
quitAppDialog.open()
|
||||
}
|
||||
visible: model.running
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: appGrid.parent
|
||||
anchors {
|
||||
top: appGrid.top
|
||||
left: appGrid.right
|
||||
bottom: appGrid.bottom
|
||||
|
||||
leftMargin: -10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
objectName: "Gamepad Mapping"
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import ComputerModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
import StreamingPreferences 1.0
|
||||
|
||||
GridView {
|
||||
property ComputerModel computerModel : createModel()
|
||||
|
||||
id: pcGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 350; cellHeight: 350;
|
||||
focus: true
|
||||
objectName: "Computers"
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Setup signals on CM
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
||||
if (!prefs.hasAnyHardwareAcceleration()) {
|
||||
noHwDecoderDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Start polling when this view is shown
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
else {
|
||||
// Stop polling when this view is not top-most
|
||||
ComputerManager.stopPollingAsync()
|
||||
}
|
||||
}
|
||||
|
||||
function pairingComplete(error)
|
||||
{
|
||||
// Close the PIN dialog
|
||||
pairDialog.close()
|
||||
|
||||
// Start polling again
|
||||
ComputerManager.startPolling()
|
||||
|
||||
// Display a failed dialog if we got an error
|
||||
if (error !== null) {
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function addComplete(success)
|
||||
{
|
||||
if (!success) {
|
||||
errorDialog.text = "Unable to connect to the specified PC. Ensure GameStream is enabled in GeForce Experience."
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function createModel()
|
||||
{
|
||||
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
||||
model.initialize(ComputerManager)
|
||||
model.pairingCompleted.connect(pairingComplete)
|
||||
return model
|
||||
}
|
||||
|
||||
model: computerModel
|
||||
|
||||
delegate: Item {
|
||||
width: 300; height: 300;
|
||||
|
||||
Image {
|
||||
id: pcIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter;
|
||||
source: {
|
||||
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
||||
}
|
||||
sourceSize {
|
||||
width: 200
|
||||
height: 200
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: pcNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
|
||||
width: parent.width
|
||||
anchors.top: pcIcon.bottom
|
||||
font.pointSize: 36
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Text {
|
||||
function getStatusText(model)
|
||||
{
|
||||
if (model.online) {
|
||||
var text = "<font color=\"green\">Online</font>"
|
||||
text += "<font color=\"white\"> - </font>"
|
||||
if (model.paired) {
|
||||
text += "<font color=\"skyblue\">Paired</font>"
|
||||
}
|
||||
else if (model.busy) {
|
||||
text += "<font color=\"red\">Busy</font>"
|
||||
}
|
||||
else {
|
||||
text += "<font color=\"red\">Not Paired</font>"
|
||||
}
|
||||
return text
|
||||
}
|
||||
else {
|
||||
return "<font color=\"red\">Offline</font>";
|
||||
}
|
||||
}
|
||||
|
||||
id: pcPairedText
|
||||
text: getStatusText(model)
|
||||
visible: !model.addPc
|
||||
|
||||
width: parent.width
|
||||
anchors.top: pcNameText.bottom
|
||||
font.pointSize: 24
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem {
|
||||
text: "Delete PC"
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
||||
onClicked: {
|
||||
if(mouse.button === Qt.LeftButton) {
|
||||
if (model.addPc) {
|
||||
addPcDialog.open()
|
||||
}
|
||||
else if (model.online) {
|
||||
if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView)
|
||||
appView.computerIndex = index
|
||||
appView.objectName = model.name
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||
|
||||
// Stop polling, since pairing may make GFE unresponsive
|
||||
ComputerManager.stopPollingAsync()
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
} else if(!model.online) {
|
||||
pcContextMenu.open()
|
||||
}
|
||||
|
||||
}
|
||||
else { // right click
|
||||
if(!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: noHwDecoderDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Warning
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration. " +
|
||||
"Click the Help button for more information on solving this problem."
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: pairDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
property string pin : "0000"
|
||||
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
||||
standardButtons: StandardButton.Cancel
|
||||
onRejected: {
|
||||
// FIXME: We should interrupt pairing here
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: deletePcDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
property int pcIndex : -1;
|
||||
text:"Are you sure you want to remove this PC?"
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: {
|
||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||
computerModel.deleteComputer(pcIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: addPcDialog
|
||||
property string label: "Enter the IP address of your GameStream PC"
|
||||
property string hint: "192.168.1.100"
|
||||
property alias editText : editTextItem
|
||||
|
||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
||||
onVisibleChanged: {
|
||||
editTextItem.focus = true
|
||||
editTextItem.selectAll()
|
||||
}
|
||||
onAccepted: {
|
||||
ComputerManager.addNewHost(editText.text, false)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Text {
|
||||
id: addPcDialogLabel
|
||||
text: addPcDialog.label
|
||||
}
|
||||
Rectangle {
|
||||
implicitWidth: parent.parent.width
|
||||
height: editTextItem.height
|
||||
|
||||
TextInput {
|
||||
id: editTextItem
|
||||
inputMethodHints: Qt.ImhPreferUppercase
|
||||
text: addPcDialog.hint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: pcGrid.parent
|
||||
anchors {
|
||||
top: pcGrid.top
|
||||
left: pcGrid.right
|
||||
bottom: pcGrid.bottom
|
||||
|
||||
leftMargin: -10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
property string appName
|
||||
property Session nextSession : null
|
||||
property string nextAppName : ""
|
||||
|
||||
property string stageText : "Quitting " + appName + "..."
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
function quitAppCompleted(error)
|
||||
{
|
||||
// Display a failed dialog if we got an error
|
||||
if (error !== undefined) {
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// If we're supposed to launch another game after this, do so now
|
||||
if (error === undefined && nextSession !== null) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = nextAppName
|
||||
segue.session = nextSession
|
||||
stackView.push(segue)
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
|
||||
// We must be polling or we won't get the quitAppCompleted() callback
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
else {
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
|
||||
// Stop polling when this view is not on top
|
||||
ComputerManager.stopPollingAsync()
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
text: stageText
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import StreamingPreferences 1.0
|
||||
|
||||
ScrollView {
|
||||
id: settingsPage
|
||||
objectName: "Settings"
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
prefs.save()
|
||||
}
|
||||
|
||||
Column {
|
||||
padding: 10
|
||||
id: settingsColumn1
|
||||
width: settingsPage.width / 2 - padding
|
||||
|
||||
GroupBox {
|
||||
id: basicSettingsGroupBox
|
||||
width: (parent.width - 2 * parent.padding)
|
||||
padding: 12
|
||||
title: "<font color=\"skyblue\">Basic Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: resFPStitle
|
||||
text: qsTr("Resolution and FPS")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: resFPSdesc
|
||||
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors")
|
||||
font.pointSize: 9
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 5
|
||||
|
||||
ComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
// load the saved width/height, and iterate through the ComboBox until a match is found
|
||||
// and set it to that index.
|
||||
var saved_width = prefs.width
|
||||
var saved_height = prefs.height
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < resolutionComboBox.count; i++) {
|
||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||
if (saved_width === el_width && saved_height === el_height) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: resolutionComboBox
|
||||
font.pointSize: 9
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: resolutionListModel
|
||||
ListElement {
|
||||
text: "720p"
|
||||
video_width: "1280"
|
||||
video_height: "720"
|
||||
}
|
||||
ListElement {
|
||||
text: "1080p"
|
||||
video_width: "1920"
|
||||
video_height: "1080"
|
||||
}
|
||||
ListElement {
|
||||
text: "1440p"
|
||||
video_width: "2560"
|
||||
video_height: "1440"
|
||||
}
|
||||
ListElement {
|
||||
text: "4K"
|
||||
video_width: "3840"
|
||||
video_height: "2160"
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||
|
||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||
slider.value = prefs.bitrateKbps
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
// Get the max supported FPS on this system
|
||||
var max_fps = prefs.getMaximumStreamingFrameRate();
|
||||
|
||||
// Use 64 as the cutoff for adding a separate option to
|
||||
// handle wonky displays that report just over 60 Hz.
|
||||
if (max_fps > 64) {
|
||||
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
||||
}
|
||||
|
||||
var saved_fps = prefs.fps
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < fpsComboBox.count; i++) {
|
||||
var el_fps = parseInt(fpsListModel.get(i).video_fps);
|
||||
if (el_fps === saved_fps) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: fpsComboBox
|
||||
font.pointSize: 9
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: fpsListModel
|
||||
ListElement {
|
||||
text: "30 FPS"
|
||||
video_fps: "30"
|
||||
}
|
||||
ListElement {
|
||||
text: "60 FPS"
|
||||
video_fps: "60"
|
||||
}
|
||||
// A higher value may be added at runtime
|
||||
// based on the attached display refresh rate
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
|
||||
|
||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||
slider.value = prefs.bitrateKbps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: bitrateTitle
|
||||
text: qsTr("Video bitrate: ")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: bitrateDesc
|
||||
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.")
|
||||
font.pointSize: 9
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
wheelEnabled: true
|
||||
|
||||
value: prefs.bitrateKbps
|
||||
|
||||
stepSize: 500
|
||||
from : 500
|
||||
to: 150000
|
||||
|
||||
snapMode: "SnapOnRelease"
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
|
||||
onValueChanged: {
|
||||
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
|
||||
prefs.bitrateKbps = value
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: fullScreenCheck
|
||||
text: "<font color=\"white\">Full-screen</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.fullScreen
|
||||
onCheckedChanged: {
|
||||
prefs.fullScreen = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
|
||||
id: audioSettingsGroupBox
|
||||
width: (parent.width - 2 * parent.padding)
|
||||
padding: 12
|
||||
title: "<font color=\"skyblue\">Audio Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: resAudioTitle
|
||||
text: qsTr("Audio configuration")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var saved_audio = prefs.audioConfig
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < audioListModel.count; i++) {
|
||||
var el_audio= audioListModel.get(i).val;
|
||||
if(saved_audio === el_audio) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: audioComboBox
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
font.pointSize: 9
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: audioListModel
|
||||
ListElement {
|
||||
text: "Autodetect"
|
||||
val: StreamingPreferences.AC_AUTO
|
||||
}
|
||||
ListElement {
|
||||
text: "Stereo"
|
||||
val: StreamingPreferences.AC_FORCE_STEREO
|
||||
}
|
||||
ListElement {
|
||||
text: "5.1 surround sound"
|
||||
val: StreamingPreferences.AC_FORCE_SURROUND
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.audioConfig = audioListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
padding: 10
|
||||
anchors.left: settingsColumn1.right
|
||||
id: settingsColumn2
|
||||
width: settingsPage.width / 2 - padding
|
||||
|
||||
GroupBox {
|
||||
id: gamepadSettingsGroupBox
|
||||
width: (parent.width - parent.padding)
|
||||
padding: 12
|
||||
title: "<font color=\"skyblue\">Gamepad Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
CheckBox {
|
||||
id: multiControllerCheck
|
||||
text: "<font color=\"white\">Multiple controller support</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.multiController
|
||||
onCheckedChanged: {
|
||||
prefs.multiController = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
id: hostSettingsGroupBox
|
||||
width: (parent.width - parent.padding)
|
||||
padding: 12
|
||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
CheckBox {
|
||||
id: optimizeGameSettingsCheck
|
||||
text: "<font color=\"white\">Optimize game settings</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.gameOptimizations
|
||||
onCheckedChanged: {
|
||||
prefs.gameOptimizations = checked
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: audioPcCheck
|
||||
text: "<font color=\"white\">Play audio on host PC</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.playAudioOnHost
|
||||
onCheckedChanged: {
|
||||
prefs.playAudioOnHost = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupBox {
|
||||
id: advancedSettingsGroupBox
|
||||
width: (parent.width - parent.padding)
|
||||
padding: 12
|
||||
title: "<font color=\"skyblue\">Advanced Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: resVDSTitle
|
||||
text: qsTr("Video decoder")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var saved_vds = prefs.videoDecoderSelection
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < decoderListModel.count; i++) {
|
||||
var el_vds = decoderListModel.get(i).val;
|
||||
if(saved_vds === el_vds) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: decoderComboBox
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
font.pointSize: 9
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: decoderListModel
|
||||
ListElement {
|
||||
text: "Automatic (Recommended)"
|
||||
val: StreamingPreferences.VDS_AUTO
|
||||
}
|
||||
ListElement {
|
||||
text: "Force software decoding"
|
||||
val: StreamingPreferences.VDS_FORCE_SOFTWARE
|
||||
}
|
||||
ListElement {
|
||||
text: "Force hardware decoding"
|
||||
val: StreamingPreferences.VDS_FORCE_HARDWARE
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: resVCCTitle
|
||||
text: qsTr("Video codec")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var saved_vcc = prefs.videoCodecConfig
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < codecListModel.count; i++) {
|
||||
var el_vcc = codecListModel.get(i).val;
|
||||
if(saved_vcc === el_vcc) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id: codecComboBox
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
font.pointSize: 9
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: codecListModel
|
||||
ListElement {
|
||||
text: "Automatic (Recommended)"
|
||||
val: StreamingPreferences.VCC_AUTO
|
||||
}
|
||||
ListElement {
|
||||
text: "Force H.264"
|
||||
val: StreamingPreferences.VCC_FORCE_H264
|
||||
}
|
||||
ListElement {
|
||||
text: "Force HEVC"
|
||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||
}
|
||||
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
|
||||
// we'll probably need to gate this feature on OS support in our
|
||||
// renderers.
|
||||
/* ListElement {
|
||||
text: "Force HEVC HDR"
|
||||
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
} */
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
property Session session
|
||||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
function stageStarting(stage)
|
||||
{
|
||||
// Update the spinner text
|
||||
stageText = "Starting " + stage + "..."
|
||||
}
|
||||
|
||||
function stageFailed(stage, errorCode)
|
||||
{
|
||||
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function connectionStarted()
|
||||
{
|
||||
// Hide the UI contents so the user doesn't
|
||||
// see them briefly when we pop off the StackView
|
||||
stageSpinner.visible = false
|
||||
stageLabel.visible = false
|
||||
|
||||
// Hide the window now that streaming has begun
|
||||
window.visible = false
|
||||
}
|
||||
|
||||
function displayLaunchError(text)
|
||||
{
|
||||
errorDialog.text = text
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
{
|
||||
// TODO: toast
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// Hook up our signals
|
||||
session.stageStarting.connect(stageStarting)
|
||||
session.stageFailed.connect(stageFailed)
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec()
|
||||
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
}
|
||||
else {
|
||||
// Show the toolbar again when we become hidden
|
||||
toolBar.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
text: stageText
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
#include "appmodel.h"
|
||||
|
||||
AppModel::AppModel(QObject *parent)
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
connect(&m_BoxArtManager, &BoxArtManager::boxArtLoadComplete,
|
||||
this, &AppModel::handleBoxArtLoaded);
|
||||
}
|
||||
|
||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
||||
{
|
||||
m_ComputerManager = computerManager;
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
this, &AppModel::handleComputerStateChanged);
|
||||
|
||||
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
||||
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
||||
m_Apps = m_Computer->appList;
|
||||
m_CurrentGameId = m_Computer->currentGameId;
|
||||
}
|
||||
|
||||
int AppModel::getRunningAppIndex()
|
||||
{
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
QString AppModel::getRunningAppName()
|
||||
{
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
return m_Apps[i].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Session* AppModel::createSessionForApp(int appIndex)
|
||||
{
|
||||
Q_ASSERT(appIndex < m_Apps.count());
|
||||
NvApp app = m_Apps.at(appIndex);
|
||||
|
||||
return new Session(m_Computer, app);
|
||||
}
|
||||
|
||||
int AppModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
// For list models only the root node (an invalid parent) should return the list's size. For all
|
||||
// other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
return m_Apps.count();
|
||||
}
|
||||
|
||||
QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
Q_ASSERT(index.row() < m_Apps.count());
|
||||
NvApp app = m_Apps.at(index.row());
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case NameRole:
|
||||
return app.name;
|
||||
case RunningRole:
|
||||
return m_Computer->currentGameId == app.id;
|
||||
case BoxArtRole:
|
||||
// FIXME: const-correctness
|
||||
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> AppModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> names;
|
||||
|
||||
names[NameRole] = "name";
|
||||
names[RunningRole] = "running";
|
||||
names[BoxArtRole] = "boxart";
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
void AppModel::quitRunningApp()
|
||||
{
|
||||
m_ComputerManager->quitRunningApp(m_Computer);
|
||||
}
|
||||
|
||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
// Ignore updates for computers that aren't ours
|
||||
if (computer != m_Computer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First, process additions/removals from the app list. This
|
||||
// is required because the new game may now be running, so
|
||||
// we can't check that first.
|
||||
if (computer->appList != m_Apps) {
|
||||
// Just reset the whole thing if the list changes
|
||||
beginResetModel();
|
||||
m_Apps = computer->appList;
|
||||
m_CurrentGameId = computer->currentGameId;
|
||||
endResetModel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally, process changes to the active app
|
||||
if (computer->currentGameId != m_CurrentGameId) {
|
||||
// First, invalidate the running state of newly running game
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == computer->currentGameId) {
|
||||
emit dataChanged(createIndex(i, 0),
|
||||
createIndex(i, 0),
|
||||
QVector<int>() << RunningRole);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Next, invalidate the running state of the old game (if it exists)
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
emit dataChanged(createIndex(i, 0),
|
||||
createIndex(i, 0),
|
||||
QVector<int>() << RunningRole);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now update our internal state
|
||||
m_CurrentGameId = m_Computer->currentGameId;
|
||||
}
|
||||
}
|
||||
|
||||
void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image */)
|
||||
{
|
||||
Q_ASSERT(computer == m_Computer);
|
||||
|
||||
int index = m_Apps.indexOf(app);
|
||||
|
||||
// Make sure we're not delivering a callback to an app that's already been removed
|
||||
if (index >= 0) {
|
||||
// Let our view know the box art data has changed for this app
|
||||
emit dataChanged(createIndex(index, 0),
|
||||
createIndex(index, 0),
|
||||
QVector<int>() << BoxArtRole);
|
||||
}
|
||||
else {
|
||||
qWarning() << "App not found for box art callback:" << app.name;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "backend/boxartmanager.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class AppModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
enum Roles
|
||||
{
|
||||
NameRole = Qt::UserRole,
|
||||
RunningRole,
|
||||
BoxArtRole
|
||||
};
|
||||
|
||||
public:
|
||||
explicit AppModel(QObject *parent = nullptr);
|
||||
|
||||
// Must be called before any QAbstractListModel functions
|
||||
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
|
||||
|
||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
||||
|
||||
Q_INVOKABLE int getRunningAppIndex();
|
||||
|
||||
Q_INVOKABLE QString getRunningAppName();
|
||||
|
||||
Q_INVOKABLE void quitRunningApp();
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
virtual QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
private slots:
|
||||
void handleComputerStateChanged(NvComputer* computer);
|
||||
|
||||
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
BoxArtManager m_BoxArtManager;
|
||||
ComputerManager* m_ComputerManager;
|
||||
QVector<NvApp> m_Apps;
|
||||
int m_CurrentGameId;
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
#include "computermodel.h"
|
||||
|
||||
ComputerModel::ComputerModel(QObject* object)
|
||||
: QAbstractListModel(object) {}
|
||||
|
||||
void ComputerModel::initialize(ComputerManager* computerManager)
|
||||
{
|
||||
m_ComputerManager = computerManager;
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
this, &ComputerModel::handleComputerStateChanged);
|
||||
connect(m_ComputerManager, &ComputerManager::pairingCompleted,
|
||||
this, &ComputerModel::handlePairingCompleted);
|
||||
|
||||
m_Computers = m_ComputerManager->getComputers();
|
||||
}
|
||||
|
||||
QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (index.row() == m_Computers.count()) {
|
||||
// We insert a synthetic item at the end for the Add PC option
|
||||
switch (role) {
|
||||
case NameRole:
|
||||
return "Add PC";
|
||||
case AddPcRole:
|
||||
return true;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
Q_ASSERT(index.row() < m_Computers.count());
|
||||
|
||||
NvComputer* computer = m_Computers[index.row()];
|
||||
QReadLocker lock(&computer->lock);
|
||||
|
||||
switch (role) {
|
||||
case NameRole:
|
||||
return computer->name;
|
||||
case OnlineRole:
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
case PairedRole:
|
||||
return computer->pairState == NvComputer::PS_PAIRED;
|
||||
case BusyRole:
|
||||
return computer->currentGameId != 0;
|
||||
case WakeableRole:
|
||||
return !computer->macAddress.isEmpty();
|
||||
case AddPcRole:
|
||||
return false;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
int ComputerModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
// We should not return a count for valid index values,
|
||||
// only the parent (which will not have a "valid" index).
|
||||
if (parent.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Add PC placeholder counts as 1
|
||||
return m_Computers.count() + 1;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> names;
|
||||
|
||||
names[NameRole] = "name";
|
||||
names[OnlineRole] = "online";
|
||||
names[PairedRole] = "paired";
|
||||
names[BusyRole] = "busy";
|
||||
names[AddPcRole] = "addPc";
|
||||
names[WakeableRole] = "wakeable";
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
Session* ComputerModel::createSessionForCurrentGame(int computerIndex)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
NvComputer* computer = m_Computers[computerIndex];
|
||||
|
||||
// We must currently be streaming a game to use this function
|
||||
Q_ASSERT(computer->currentGameId != 0);
|
||||
|
||||
for (NvApp& app : computer->appList) {
|
||||
if (app.id == computer->currentGameId) {
|
||||
return new Session(computer, app);
|
||||
}
|
||||
}
|
||||
|
||||
// We have a current running app but it's not in our app list
|
||||
Q_ASSERT(false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ComputerModel::deleteComputer(int computerIndex)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
beginRemoveRows(QModelIndex(), computerIndex, computerIndex);
|
||||
|
||||
// m_Computer[computerIndex] will be deleted by this call
|
||||
m_ComputerManager->deleteHost(m_Computers[computerIndex]);
|
||||
|
||||
// Remove the now invalid item
|
||||
m_Computers.removeAt(computerIndex);
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
bool ComputerModel::wakeComputer(int computerIndex)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
return m_Computers[computerIndex]->wake();
|
||||
}
|
||||
|
||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
m_ComputerManager->pairHost(m_Computers[computerIndex], pin);
|
||||
}
|
||||
|
||||
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||
{
|
||||
emit pairingCompleted(error.isNull() ? QVariant() : error);
|
||||
}
|
||||
|
||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
// If this is an existing computer, we can report the data changed
|
||||
int index = m_Computers.indexOf(computer);
|
||||
if (index >= 0) {
|
||||
// Let the view know that this specific computer changed
|
||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||
}
|
||||
else {
|
||||
// This is a new PC which may be inserted at an arbitrary point
|
||||
// in our computer list (since it comes from CM's QMap). Reload
|
||||
// the whole model state to ensure it stays consistent.
|
||||
beginResetModel();
|
||||
m_Computers = m_ComputerManager->getComputers();
|
||||
endResetModel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
class ComputerModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
enum Roles
|
||||
{
|
||||
NameRole = Qt::UserRole,
|
||||
OnlineRole,
|
||||
PairedRole,
|
||||
BusyRole,
|
||||
WakeableRole,
|
||||
AddPcRole
|
||||
};
|
||||
|
||||
public:
|
||||
explicit ComputerModel(QObject* object = nullptr);
|
||||
|
||||
// Must be called before any QAbstractListModel functions
|
||||
Q_INVOKABLE void initialize(ComputerManager* computerManager);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
|
||||
virtual QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void deleteComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||
|
||||
Q_INVOKABLE bool wakeComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||
|
||||
signals:
|
||||
void pairingCompleted(QVariant error);
|
||||
|
||||
private slots:
|
||||
void handleComputerStateChanged(NvComputer* computer);
|
||||
|
||||
void handlePairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
private:
|
||||
QVector<NvComputer*> m_Computers;
|
||||
ComputerManager* m_ComputerManager;
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import QtQuick.Controls.Material 2.1
|
||||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
visible: true
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.Purple
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: "PcView.qml"
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
function navigateTo(url, objectName)
|
||||
{
|
||||
var existingItem = stackView.find(function(item, index) {
|
||||
return item.objectName === objectName
|
||||
})
|
||||
|
||||
if (existingItem !== null) {
|
||||
// Pop to the existing item
|
||||
stackView.pop(existingItem)
|
||||
}
|
||||
else {
|
||||
// Create a new item
|
||||
stackView.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
header: ToolBar {
|
||||
id: toolBar
|
||||
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
anchors.fill: parent
|
||||
|
||||
ToolButton {
|
||||
// Only make the button visible if the user has navigated somewhere.
|
||||
visible: stackView.depth > 1
|
||||
|
||||
icon.source: "qrc:/res/arrow_left.svg"
|
||||
onClicked: stackView.pop()
|
||||
|
||||
Menu {
|
||||
id: backButton
|
||||
x: parent.width - width
|
||||
transformOrigin: Menu.TopRight
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: titleLabel
|
||||
text: stackView.currentItem.objectName
|
||||
font.pointSize: 15
|
||||
elide: Label.ElideRight
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: "qrc:/res/question_mark.svg"
|
||||
|
||||
// TODO need to make sure browser is brought to foreground.
|
||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||
|
||||
Menu {
|
||||
id: helpButton
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
// TODO: Implement gamepad mapping then unhide this button
|
||||
visible: false
|
||||
|
||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||
|
||||
Menu {
|
||||
id: gamepadMappingMenu
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: "qrc:/res/settings.svg"
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||
|
||||
Menu {
|
||||
id: optionsMenu
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QIcon>
|
||||
#include <QQuickStyle>
|
||||
#include <QMutex>
|
||||
#include <QtDebug>
|
||||
|
||||
// Don't let SDL hook our main function, since Qt is already
|
||||
// doing the same thing. This needs to be before any headers
|
||||
// that might include SDL.h themselves.
|
||||
#define SDL_MAIN_HANDLED
|
||||
#include <SDL.h>
|
||||
|
||||
#include "gui/computermodel.h"
|
||||
#include "gui/appmodel.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||
// Log to file for release Windows builds
|
||||
#define USE_CUSTOM_LOGGER
|
||||
#define LOG_TO_FILE
|
||||
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
|
||||
// Use stdout logger on all Linux/BSD builds
|
||||
#define USE_CUSTOM_LOGGER
|
||||
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
|
||||
// Log to file for release Mac builds
|
||||
#define USE_CUSTOM_LOGGER
|
||||
#define LOG_TO_FILE
|
||||
#else
|
||||
// For debug Windows and Mac builds, use default logger
|
||||
#endif
|
||||
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
static QTime s_LoggerTime;
|
||||
static QTextStream s_LoggerStream(stdout);
|
||||
static QMutex s_LoggerLock;
|
||||
#ifdef LOG_TO_FILE
|
||||
#define MAX_LOG_LINES 10000
|
||||
static int s_LogLinesWritten = 0;
|
||||
static bool s_LogLimitReached = false;
|
||||
static QFile* s_LoggerFile;
|
||||
#endif
|
||||
|
||||
void logToLoggerStream(QString& message)
|
||||
{
|
||||
QMutexLocker lock(&s_LoggerLock);
|
||||
|
||||
#ifdef LOG_TO_FILE
|
||||
if (s_LogLimitReached) {
|
||||
return;
|
||||
}
|
||||
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
||||
s_LoggerStream << "Log size limit reached!" << endl;
|
||||
s_LogLimitReached = true;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
s_LogLinesWritten++;
|
||||
}
|
||||
#endif
|
||||
|
||||
s_LoggerStream << message << endl;
|
||||
}
|
||||
|
||||
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||
{
|
||||
QString priorityTxt;
|
||||
|
||||
switch (priority) {
|
||||
case SDL_LOG_PRIORITY_VERBOSE:
|
||||
priorityTxt = "Verbose";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_DEBUG:
|
||||
priorityTxt = "Debug";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_INFO:
|
||||
priorityTxt = "Info";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_WARN:
|
||||
priorityTxt = "Warn";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_ERROR:
|
||||
priorityTxt = "Error";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_CRITICAL:
|
||||
priorityTxt = "Critical";
|
||||
break;
|
||||
default:
|
||||
priorityTxt = "Unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
|
||||
void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
|
||||
{
|
||||
QString typeTxt;
|
||||
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
typeTxt = "Debug";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
typeTxt = "Info";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
typeTxt = "Warning";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
typeTxt = "Critical";
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
typeTxt = "Fatal";
|
||||
break;
|
||||
}
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
#ifdef LOG_TO_FILE
|
||||
QDir tempDir(QDir::tempPath());
|
||||
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
||||
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
|
||||
s_LoggerStream.setDevice(s_LoggerFile);
|
||||
}
|
||||
#endif
|
||||
|
||||
s_LoggerTime.start();
|
||||
qInstallMessageHandler(qtLogToDiskHandler);
|
||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
#endif
|
||||
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
|
||||
// This avoids using the default keychain for SSL, which may cause
|
||||
// password prompts on macOS.
|
||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
||||
|
||||
// Set these here to allow us to use the default QSettings constructor
|
||||
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
||||
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
||||
QCoreApplication::setApplicationName("Moonlight");
|
||||
|
||||
// Register custom metatypes for use in signals
|
||||
qRegisterMetaType<NvApp>("NvApp");
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||
|
||||
// Register our C++ types for QML
|
||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||
"ComputerManager",
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new ComputerManager();
|
||||
});
|
||||
|
||||
QQuickStyle::setStyle("Material");
|
||||
|
||||
// Load the main.qml file
|
||||
QQmlApplicationEngine engine;
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
|
||||
SDL_SetMainReady();
|
||||
if (SDL_Init(0) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_Init() failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
int err = app.exec();
|
||||
|
||||
SDL_Quit();
|
||||
|
||||
return err;
|
||||
}
|
||||