aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSylvain CECCHETTO <cecchetto.sylvain@me.com>2019-04-28 23:47:02 +0200
committerBrent Murphy <bmurphy@bcmcs.net>2020-01-08 19:08:11 +1000
commit5b152cd2f32568e87ee8ba0b3e7f9870028dcca2 (patch)
tree28c0701124a63b6605d64df1a69a379e9eb0d06c
parent4258a6d587372965512a221b725ec127161fa331 (diff)
[tvOS] Initial Platform Commit for Apple TVOS
Initial commit for TVOS platform.
-rw-r--r--addons/resource.language.en_gb/resources/strings.po56
-rw-r--r--cmake/platform/darwin_embedded/tvos.cmake3
-rw-r--r--cmake/scripts/darwin_embedded/ArchSetup.cmake7
-rw-r--r--cmake/scripts/darwin_embedded/ExtraTargets.cmake25
-rw-r--r--cmake/scripts/darwin_embedded/Install.cmake105
-rw-r--r--[l---------]cmake/scripts/darwin_embedded/Macros.cmake119
-rw-r--r--cmake/scripts/darwin_embedded/PathSetup.cmake3
-rw-r--r--cmake/treedata/darwin_embedded/ios/ios.txt2
-rw-r--r--cmake/treedata/darwin_embedded/subdirs.txt2
-rwxr-xr-xcmake/treedata/darwin_embedded/tvos/tvos.txt4
-rw-r--r--docs/README.md1
-rw-r--r--docs/README.tvOS.md229
-rw-r--r--docs/resources/TvOS.svg33
-rw-r--r--system/keymaps/customcontroller.SiriRemote.xml145
-rw-r--r--system/settings/darwin_tvos.xml140
-rwxr-xr-xtools/buildsteps/tvos/configure-xbmc5
-rwxr-xr-xtools/buildsteps/tvos/make-xbmc7
-rwxr-xr-xtools/buildsteps/tvos/package12
-rwxr-xr-xtools/darwin/Support/Codesign.command84
-rwxr-xr-xtools/darwin/Support/GenerateMissingImages-tvos.py29
-rwxr-xr-xtools/darwin/Support/copyframeworks-darwin_embedded.command12
-rwxr-xr-xtools/darwin/Support/copyframeworks-dylibs2frameworks.command106
-rw-r--r--tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh.in37
-rw-r--r--tools/depends/target/Makefile1
-rw-r--r--tools/depends/target/pythonmodule-pil/Makefile8
-rw-r--r--xbmc/Application.h3
-rw-r--r--xbmc/GUIInfoManager.cpp10
-rw-r--r--xbmc/Util.cpp2
-rw-r--r--xbmc/Util.h2
-rw-r--r--xbmc/addons/addoninfo/AddonInfoBuilder.cpp8
-rw-r--r--xbmc/cores/AudioEngine/CMakeLists.txt13
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h57
-rw-r--r--xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.mm895
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt1
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp8
-rw-r--r--xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt1
-rw-r--r--xbmc/filesystem/DirectoryFactory.cpp11
-rw-r--r--xbmc/filesystem/FileFactory.cpp12
-rw-r--r--xbmc/guilib/GUIKeyboardFactory.cpp18
-rw-r--r--xbmc/guilib/TextureManager.cpp15
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoLabels.h2
-rw-r--r--xbmc/guilib/guiinfo/SystemGUIInfo.cpp7
-rw-r--r--xbmc/platform/darwin/DarwinUtils.mm7
-rw-r--r--xbmc/platform/darwin/ios-common/AnnounceReceiver.mm5
-rw-r--r--xbmc/platform/darwin/ios-common/CMakeLists.txt6
-rw-r--r--xbmc/platform/darwin/ios-common/DarwinEmbedKeyboard.mm17
-rw-r--r--xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.h1
-rw-r--r--xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.mm27
-rw-r--r--xbmc/platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.mm12
-rw-r--r--xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.h32
-rw-r--r--xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.mm199
-rw-r--r--xbmc/platform/darwin/ios-common/NSData+GZIP.h43
-rw-r--r--xbmc/platform/darwin/ios-common/NSData+GZIP.m135
-rw-r--r--xbmc/platform/darwin/ios/IOSKeyboardView.mm30
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/Contents.json32
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Contents.json26
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/.gitignore1
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/Contents.json18
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/image@2x.pngbin0 -> 10949 bytes
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/.gitignore1
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/Contents.json18
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/image@2x.pngbin0 -> 10200 bytes
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/.gitignore1
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/Contents.json18
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/image@2x.pngbin0 -> 66421 bytes
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/.gitignore1
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/Contents.json18
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/image@2x.pngbin0 -> 141426 bytes
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Content.imageset/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Contents.json6
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Contents.json17
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Content.imageset/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Contents.json6
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Contents.json6
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf.imageset/Contents.json16
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/.gitignore1
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/Contents.json18
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/image.pngbin0 -> 616832 bytes
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/Contents.json6
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/.gitignore1
-rw-r--r--xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json24
l---------xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/splash.jpg1
-rw-r--r--xbmc/platform/darwin/tvos/CMakeLists.txt20
-rw-r--r--xbmc/platform/darwin/tvos/FrameworkSeed_Info.plist35
-rw-r--r--xbmc/platform/darwin/tvos/Info.plist.in51
-rw-r--r--xbmc/platform/darwin/tvos/Kodi.entitlements.in10
-rw-r--r--xbmc/platform/darwin/tvos/PreflightHandler.h31
-rw-r--r--xbmc/platform/darwin/tvos/PreflightHandler.mm191
-rw-r--r--xbmc/platform/darwin/tvos/TVOSDisplayManager.h40
-rw-r--r--xbmc/platform/darwin/tvos/TVOSDisplayManager.mm237
-rw-r--r--xbmc/platform/darwin/tvos/TVOSEAGLView.h36
-rw-r--r--xbmc/platform/darwin/tvos/TVOSEAGLView.mm195
-rw-r--r--xbmc/platform/darwin/tvos/TVOSKeyboardView.h12
-rw-r--r--xbmc/platform/darwin/tvos/TVOSKeyboardView.mm12
-rw-r--r--xbmc/platform/darwin/tvos/TVOSSettingsHandler.h28
-rw-r--r--xbmc/platform/darwin/tvos/TVOSSettingsHandler.mm71
-rw-r--r--xbmc/platform/darwin/tvos/TVOSTopShelf.h28
-rw-r--r--xbmc/platform/darwin/tvos/TVOSTopShelf.mm181
-rw-r--r--xbmc/platform/darwin/tvos/TopShelf/Info.plist.in44
-rw-r--r--xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.h25
-rw-r--r--xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.m124
-rw-r--r--xbmc/platform/darwin/tvos/TopShelf/TopShelf.entitlements.in10
-rw-r--r--xbmc/platform/darwin/tvos/XBMCApplication.h13
-rw-r--r--xbmc/platform/darwin/tvos/XBMCApplication.mm130
-rw-r--r--xbmc/platform/darwin/tvos/XBMCController.h70
-rw-r--r--xbmc/platform/darwin/tvos/XBMCController.mm544
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/CMakeLists.txt9
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.cpp121
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.h43
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/TVOSFile.cpp268
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/TVOSFile.h62
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.h16
-rw-r--r--xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.mm42
-rw-r--r--xbmc/platform/darwin/tvos/input/CMakeLists.txt11
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputHandler.h24
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputHandler.mm72
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputRemote.h29
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputRemote.mm160
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputSettings.h17
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputSettings.mm39
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputTouch.h56
-rw-r--r--xbmc/platform/darwin/tvos/input/LibInputTouch.mm646
-rw-r--r--xbmc/platform/darwin/tvos/tvosShared.h16
-rw-r--r--xbmc/platform/darwin/tvos/tvosShared.m63
-rw-r--r--xbmc/settings/DisplaySettings.cpp3
-rw-r--r--xbmc/settings/SettingConditions.cpp3
-rw-r--r--xbmc/settings/Settings.cpp20
-rw-r--r--xbmc/settings/Settings.h4
-rw-r--r--xbmc/threads/platform/pthreads/ThreadImpl.cpp2
-rw-r--r--xbmc/utils/RecentlyAddedJob.cpp9
-rw-r--r--xbmc/utils/SystemInfo.cpp20
-rw-r--r--xbmc/utils/test/TestSystemInfo.cpp9
-rw-r--r--xbmc/windowing/tvos/CMakeLists.txt10
-rw-r--r--xbmc/windowing/tvos/OSScreenSaverTVOS.h19
-rw-r--r--xbmc/windowing/tvos/OSScreenSaverTVOS.mm21
-rw-r--r--xbmc/windowing/tvos/VideoSyncTVos.cpp93
-rw-r--r--xbmc/windowing/tvos/VideoSyncTVos.h43
-rw-r--r--xbmc/windowing/tvos/WinEventsTVOS.h35
-rw-r--r--xbmc/windowing/tvos/WinEventsTVOS.mm76
-rw-r--r--xbmc/windowing/tvos/WinSystemTVOS.h103
-rw-r--r--xbmc/windowing/tvos/WinSystemTVOS.mm442
146 files changed, 7576 insertions, 270 deletions
diff --git a/addons/resource.language.en_gb/resources/strings.po b/addons/resource.language.en_gb/resources/strings.po
index bc66b370a0..553ab3370f 100644
--- a/addons/resource.language.en_gb/resources/strings.po
+++ b/addons/resource.language.en_gb/resources/strings.po
@@ -15602,7 +15602,61 @@ msgctxt "#24152"
msgid "The add-on is not compatible with this version of Kodi."
msgstr ""
-#empty strings from id 24153 to 24990
+#: apple remote
+msgctxt "#24153"
+msgid "Enable to make the Siri remote match the normal Apple tvOS behavior"
+msgstr ""
+
+#: apple remote
+msgctxt "#24154"
+msgid "Idle remote handling after N seconds"
+msgstr ""
+
+#: apple remote
+msgctxt "#24155"
+msgid "After remote idles, the first tap/swipe received will wake it"
+msgstr ""
+
+#: apple remote
+msgctxt "#24156"
+msgid "30 Seconds"
+msgstr ""
+
+#: apple remote
+msgctxt "#24157"
+msgid "60 Seconds"
+msgstr ""
+
+#: apple remote
+msgctxt "#24158"
+msgid "90 Seconds"
+msgstr ""
+
+#: apple remote
+msgctxt "#24159"
+msgid "120 Seconds"
+msgstr ""
+
+#: apple remote
+msgctxt "#24160"
+msgid "Enable Siri remote timeout"
+msgstr ""
+
+#: apple remote
+msgctxt "#24161"
+msgid "Enable remote input timeout for tap/swipe"
+msgstr ""
+
+msgctxt "#24162"
+msgid "Use Kodi virtual keyboard"
+msgstr ""
+
+#: apple remote
+msgctxt "#24163"
+msgid "Match Apple tvOS Standard (Siri remote)"
+msgstr ""
+
+#empty strings from id 24164 to 24990
#. Used as error message in add-on browser when add-on repository data could not be downloaded
#: xbmc/filesystem/AddonsDirectory.cpp
diff --git a/cmake/platform/darwin_embedded/tvos.cmake b/cmake/platform/darwin_embedded/tvos.cmake
new file mode 100644
index 0000000000..b29ed3279b
--- /dev/null
+++ b/cmake/platform/darwin_embedded/tvos.cmake
@@ -0,0 +1,3 @@
+set(PLATFORM_REQUIRED_DEPS OpenGLES)
+set(APP_RENDER_SYSTEM gles)
+list(APPEND PLATFORM_DEFINES -DTARGET_DARWIN_TVOS)
diff --git a/cmake/scripts/darwin_embedded/ArchSetup.cmake b/cmake/scripts/darwin_embedded/ArchSetup.cmake
index fd1f750ed7..a2be3b7e76 100644
--- a/cmake/scripts/darwin_embedded/ArchSetup.cmake
+++ b/cmake/scripts/darwin_embedded/ArchSetup.cmake
@@ -38,10 +38,15 @@ list(APPEND DEPLIBS "-framework CoreFoundation" "-framework CoreVideo"
"-framework CFNetwork" "-framework CoreGraphics"
"-framework Foundation" "-framework UIKit"
"-framework CoreMedia" "-framework AVFoundation"
- "-framework VideoToolbox" "-lresolv")
+ "-framework VideoToolbox" "-lresolv" "-ObjC"
+ "-framework AVKit")
set(ENABLE_OPTICAL OFF CACHE BOOL "" FORCE)
+# AppleTV already has built-in AirPlay support
+if(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ set(ENABLE_AIRTUNES OFF CACHE BOOL "" FORCE)
+endif()
set(CMAKE_XCODE_ATTRIBUTE_INLINES_ARE_PRIVATE_EXTERN OFF)
set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN OFF)
set(CMAKE_XCODE_ATTRIBUTE_COPY_PHASE_STRIP OFF)
diff --git a/cmake/scripts/darwin_embedded/ExtraTargets.cmake b/cmake/scripts/darwin_embedded/ExtraTargets.cmake
new file mode 100644
index 0000000000..2b9980a184
--- /dev/null
+++ b/cmake/scripts/darwin_embedded/ExtraTargets.cmake
@@ -0,0 +1,25 @@
+if(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ # top shelf extension
+ set(TOPSHELF_EXTENSION_NAME "${APP_NAME_LC}-topshelf")
+ set(TOPSHELF_BUNDLE_EXTENSION appex)
+ set(TOPSHELF_DIR "${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/tvos/TopShelf")
+ # same path as the output Info.plist, taken from cmGlobalXCodeGenerator::ComputeInfoPListLocation()
+ set(ENTITLEMENTS_OUT_PATH "${CMAKE_BINARY_DIR}/CMakeFiles/${TOPSHELF_EXTENSION_NAME}.dir/TopShelf.entitlements")
+
+ set(SOURCES
+ ${TOPSHELF_DIR}/ServiceProvider.m
+ ${TOPSHELF_DIR}/../tvosShared.m)
+ set(HEADERS
+ ${TOPSHELF_DIR}/ServiceProvider.h
+ ${TOPSHELF_DIR}/../tvosShared.h)
+ add_executable(${TOPSHELF_EXTENSION_NAME} MACOSX_BUNDLE ${SOURCES} ${HEADERS})
+
+ configure_file(${TOPSHELF_DIR}/TopShelf.entitlements.in ${ENTITLEMENTS_OUT_PATH} @ONLY)
+ set_target_properties(${TOPSHELF_EXTENSION_NAME} PROPERTIES BUNDLE_EXTENSION ${TOPSHELF_BUNDLE_EXTENSION}
+ MACOSX_BUNDLE_INFO_PLIST ${TOPSHELF_DIR}/Info.plist.in
+ XCODE_PRODUCT_TYPE com.apple.product-type.tv-app-extension
+ XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${ENTITLEMENTS_OUT_PATH})
+ target_link_libraries(${TOPSHELF_EXTENSION_NAME} "-framework TVServices" "-framework Foundation")
+
+ add_dependencies(${APP_NAME_LC} ${TOPSHELF_EXTENSION_NAME})
+endif()
diff --git a/cmake/scripts/darwin_embedded/Install.cmake b/cmake/scripts/darwin_embedded/Install.cmake
index 795dad68de..ae8337bfa4 100644
--- a/cmake/scripts/darwin_embedded/Install.cmake
+++ b/cmake/scripts/darwin_embedded/Install.cmake
@@ -1,31 +1,52 @@
-# IOS packaging
-
-set(BUNDLE_RESOURCES ${CMAKE_SOURCE_DIR}/media/splash.jpg
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon29x29.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon29x29@2x.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon40x40.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon40x40@2x.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon50x50.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon50x50@2x.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon57x57.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon57x57@2x.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon60x60.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon60x60@2x.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon72x72.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon72x72@2x.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon76x76.png
- ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon76x76@2x.png)
-
-target_sources(${APP_NAME_LC} PRIVATE ${BUNDLE_RESOURCES})
-foreach(file IN LISTS BUNDLE_RESOURCES)
- set_source_files_properties(${file} PROPERTIES MACOSX_PACKAGE_LOCATION .)
-endforeach()
-
-target_sources(${APP_NAME_LC} PRIVATE ${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/ios/LaunchScreen.storyboard)
-set_source_files_properties(${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/ios/LaunchScreen.storyboard PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
+# IOS/TVOS packaging
+if(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ # asset catalog
+ set(ASSET_CATALOG "${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/tvos/Assets.xcassets")
+ set(ASSET_CATALOG_ASSETS Assets)
+ set(ASSET_CATALOG_LAUNCH_IMAGE LaunchImage)
-# setup code signing
+ message("generating missing asset catalog images...")
+ execute_process(COMMAND ${CMAKE_SOURCE_DIR}/tools/darwin/Support/GenerateMissingImages-tvos.py "${ASSET_CATALOG}" ${ASSET_CATALOG_ASSETS} ${ASSET_CATALOG_LAUNCH_IMAGE})
+
+ target_sources(${APP_NAME_LC} PRIVATE "${ASSET_CATALOG}")
+ set_source_files_properties("${ASSET_CATALOG}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") # adds to Copy Bundle Resources build phase
+
+ # entitlements
+ set(ENTITLEMENTS_OUT_PATH "${CMAKE_BINARY_DIR}/CMakeFiles/${APP_NAME_LC}.dir/Kodi.entitlements")
+ configure_file(${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/tvos/Kodi.entitlements.in ${ENTITLEMENTS_OUT_PATH} @ONLY)
+
+ set_target_properties(${APP_NAME_LC} PROPERTIES XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME ${ASSET_CATALOG_ASSETS}
+ XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME ${ASSET_CATALOG_LAUNCH_IMAGE}
+ XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${ENTITLEMENTS_OUT_PATH})
+else()
+ set(BUNDLE_RESOURCES ${CMAKE_SOURCE_DIR}/media/splash.jpg
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon29x29.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon29x29@2x.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon40x40.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon40x40@2x.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon50x50.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon50x50@2x.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon57x57.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon57x57@2x.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon60x60.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon60x60@2x.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon72x72.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon72x72@2x.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon76x76.png
+ ${CMAKE_SOURCE_DIR}/tools/darwin/packaging/media/ios/rounded/AppIcon76x76@2x.png)
+
+ target_sources(${APP_NAME_LC} PRIVATE ${BUNDLE_RESOURCES})
+ foreach(file IN LISTS BUNDLE_RESOURCES)
+ set_source_files_properties(${file} PROPERTIES MACOSX_PACKAGE_LOCATION .)
+ endforeach()
+
+ target_sources(${APP_NAME_LC} PRIVATE ${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/ios/LaunchScreen.storyboard)
+ set_source_files_properties(${CMAKE_SOURCE_DIR}/xbmc/platform/darwin/ios/LaunchScreen.storyboard PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
+
+endif()
+
+# setup code signing
# dev team ID / identity (certificate)
set(DEVELOPMENT_TEAM "" CACHE STRING "Development Team")
set(CODE_SIGN_IDENTITY $<IF:$<BOOL:${DEVELOPMENT_TEAM}>,iPhone\ Developer,> CACHE STRING "Code Sign Identity")
@@ -36,6 +57,24 @@ set(PROVISIONING_PROFILE_APP "" CACHE STRING "Provisioning profile name for the
if(PROVISIONING_PROFILE_APP)
set(CODE_SIGN_STYLE_APP Manual)
endif()
+
+# top shelf provisioning profile
+if(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ set(CODE_SIGN_STYLE_TOPSHELF Automatic)
+ set(PROVISIONING_PROFILE_TOPSHELF "" CACHE STRING "Provisioning profile name for the Top Shelf")
+ if(PROVISIONING_PROFILE_TOPSHELF)
+ set(CODE_SIGN_STYLE_TOPSHELF Manual)
+ endif()
+ set_target_properties(${TOPSHELF_EXTENSION_NAME} PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${CODE_SIGN_IDENTITY}"
+ XCODE_ATTRIBUTE_CODE_SIGN_STYLE ${CODE_SIGN_STYLE_TOPSHELF}
+ XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${DEVELOPMENT_TEAM}"
+ XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "${PROVISIONING_PROFILE_TOPSHELF}")
+ # copy extension inside PlugIns dir of the app bundle
+ add_custom_command(TARGET ${APP_NAME_LC} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} ARGS -E copy_directory $<TARGET_BUNDLE_DIR:${TOPSHELF_EXTENSION_NAME}>
+ $<TARGET_BUNDLE_DIR:${APP_NAME_LC}>/PlugIns/${TOPSHELF_EXTENSION_NAME}.${TOPSHELF_BUNDLE_EXTENSION}
+ MAIN_DEPENDENCY ${TOPSHELF_EXTENSION_NAME})
+endif()
set_target_properties(${APP_NAME_LC} PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${CODE_SIGN_IDENTITY}"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE ${CODE_SIGN_STYLE_APP}
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${DEVELOPMENT_TEAM}"
@@ -53,16 +92,26 @@ add_custom_command(TARGET ${APP_NAME_LC} POST_BUILD
COMMAND "XBMC_DEPENDS=${DEPENDS_PATH}"
"PYTHON_VERSION=${PYTHON_VERSION}"
${CMAKE_SOURCE_DIR}/tools/darwin/Support/copyframeworks-darwin_embedded.command
+ COMMAND ${CMAKE_SOURCE_DIR}/tools/darwin/Support/copyframeworks-dylibs2frameworks.command
COMMAND "XBMC_DEPENDS=${DEPENDS_PATH}"
"NATIVEPREFIX=${NATIVEPREFIX}"
${CMAKE_SOURCE_DIR}/tools/darwin/Support/Codesign.command
)
+if(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ add_custom_command(TARGET ${APP_NAME_LC} POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_directory ${DEPENDS_PATH}/share/${APP_NAME_LC} $<TARGET_FILE_DIR:${APP_NAME_LC}>/AppData/AppHome
+ )
+endif()
+
set(DEPENDS_ROOT_FOR_XCODE ${NATIVEPREFIX}/..)
configure_file(${CMAKE_SOURCE_DIR}/tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh.in
${CMAKE_BINARY_DIR}/tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh @ONLY)
-configure_file(${CMAKE_SOURCE_DIR}/tools/darwin/packaging/darwin_embedded/migrate_to_kodi.sh.in
- ${CMAKE_BINARY_DIR}/tools/darwin/packaging/darwin_embedded/migrate_to_kodi.sh @ONLY)
+
+if(CORE_PLATFORM_NAME_LC STREQUAL ios)
+ configure_file(${CMAKE_SOURCE_DIR}/tools/darwin/packaging/darwin_embedded/migrate_to_kodi.sh.in
+ ${CMAKE_BINARY_DIR}/tools/darwin/packaging/darwin_embedded/migrate_to_kodi.sh @ONLY)
+endif()
add_custom_target(deb
COMMAND sh ./mkdeb-darwin_embedded.sh ${CORE_BUILD_CONFIG}
diff --git a/cmake/scripts/darwin_embedded/Macros.cmake b/cmake/scripts/darwin_embedded/Macros.cmake
index 54c1b280e0..91f2d868e7 120000..100644
--- a/cmake/scripts/darwin_embedded/Macros.cmake
+++ b/cmake/scripts/darwin_embedded/Macros.cmake
@@ -1 +1,118 @@
-../osx/Macros.cmake \ No newline at end of file
+function(core_link_library lib wraplib)
+ if(CMAKE_GENERATOR MATCHES "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL Ninja)
+ set(wrapper_obj cores/dll-loader/exports/CMakeFiles/wrapper.dir/wrapper.c.o)
+ elseif(CMAKE_GENERATOR MATCHES "Xcode")
+ set(wrapper_obj cores/dll-loader/exports/kodi.build/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/wrapper.build/Objects-$(CURRENT_VARIANT)/$(CURRENT_ARCH)/wrapper.o)
+ else()
+ message(FATAL_ERROR "Unsupported generator in core_link_library")
+ endif()
+
+ set(export -bundle -undefined dynamic_lookup
+ -Wl,-alias_list,${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/cores/dll-loader/exports/wrapper.def
+ ${CMAKE_BINARY_DIR}/${CORE_BUILD_DIR}/${wrapper_obj})
+ set(extension ${CMAKE_SHARED_MODULE_SUFFIX})
+ set(check_arg "")
+ if(TARGET ${lib})
+ set(target ${lib})
+ set(link_lib $<TARGET_FILE:${lib}>)
+ set(check_arg ${ARGV2})
+ set(data_arg ${ARGV3})
+
+ # iOS: EFFECTIVE_PLATFORM_NAME is not resolved
+ # http://public.kitware.com/pipermail/cmake/2016-March/063049.html
+ if(CORE_SYSTEM_NAME STREQUAL darwin_embedded)
+ get_target_property(dir ${lib} BINARY_DIR)
+ set(link_lib ${dir}/${CORE_BUILD_CONFIG}/${CMAKE_STATIC_LIBRARY_PREFIX}${lib}${CMAKE_STATIC_LIBRARY_SUFFIX})
+ endif()
+ else()
+ set(target ${ARGV2})
+ set(link_lib ${lib})
+ set(check_arg ${ARGV3})
+ set(data_arg ${ARGV4})
+ endif()
+ if(check_arg STREQUAL export)
+ set(export ${export}
+ -Wl,--version-script=${ARGV3})
+ elseif(check_arg STREQUAL extras)
+ foreach(arg ${data_arg})
+ list(APPEND export ${arg})
+ endforeach()
+ elseif(check_arg STREQUAL archives)
+ set(extra_libs ${data_arg})
+ endif()
+ get_filename_component(dir ${wraplib} DIRECTORY)
+
+ # We can't simply pass the linker flags to the args section of the custom command
+ # because cmake will add quotes around it (and the linker will fail due to those).
+ # We need to do this handstand first ...
+ string(REPLACE " " ";" CUSTOM_COMMAND_ARGS_LDFLAGS ${CMAKE_SHARED_LINKER_FLAGS})
+
+ add_custom_command(OUTPUT ${wraplib}-${ARCH}${extension}
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${dir}
+ COMMAND ${CMAKE_C_COMPILER}
+ ARGS ${CUSTOM_COMMAND_ARGS_LDFLAGS} ${export} -Wl,-force_load ${link_lib} ${extra_libs}
+ -o ${CMAKE_BINARY_DIR}/${wraplib}-${ARCH}${extension}
+ DEPENDS ${target} wrapper.def wrapper
+ VERBATIM)
+
+ get_filename_component(libname ${wraplib} NAME_WE)
+ add_custom_target(wrap_${libname} ALL DEPENDS ${wraplib}-${ARCH}${extension})
+ set_target_properties(wrap_${libname} PROPERTIES FOLDER lib/wrapped)
+ add_dependencies(${APP_NAME_LC}-libraries wrap_${libname})
+
+ set(LIBRARY_FILES ${LIBRARY_FILES} ${CMAKE_BINARY_DIR}/${wraplib}-${ARCH}${extension} CACHE STRING "" FORCE)
+endfunction()
+
+function(find_soname lib)
+ cmake_parse_arguments(arg "REQUIRED" "" "" ${ARGN})
+
+ string(TOLOWER ${lib} liblow)
+ if(${lib}_LDFLAGS)
+ set(link_lib "${${lib}_LDFLAGS}")
+ else()
+ set(link_lib "${${lib}_LIBRARIES}")
+ endif()
+
+ execute_process(COMMAND ${CMAKE_C_COMPILER} -print-search-dirs
+ COMMAND fgrep libraries:
+ COMMAND sed "s/[^=]*=\\(.*\\)/\\1/"
+ COMMAND sed "s/:/ /g"
+ ERROR_QUIET
+ OUTPUT_VARIABLE cc_lib_path
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ execute_process(COMMAND echo ${link_lib}
+ COMMAND sed "s/-L[ ]*//g"
+ COMMAND sed "s/-l[^ ]*//g"
+ ERROR_QUIET
+ OUTPUT_VARIABLE env_lib_path
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+
+ foreach(path ${cc_lib_path} ${env_lib_path})
+ if(IS_DIRECTORY ${path})
+ execute_process(COMMAND ls -- ${path}/lib${liblow}.dylib
+ ERROR_QUIET
+ OUTPUT_VARIABLE lib_file
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ else()
+ set(lib_file ${path})
+ endif()
+ if(lib_file)
+ # we want the path/name that is embedded in the dylib
+ execute_process(COMMAND otool -L ${lib_file}
+ COMMAND grep -v lib${liblow}.dylib
+ COMMAND grep ${liblow}
+ COMMAND awk "{V=1; print $V}"
+ ERROR_QUIET
+ OUTPUT_VARIABLE filename
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ get_filename_component(${lib}_SONAME "${filename}" NAME)
+ if(VERBOSE)
+ message(STATUS "${lib} soname: ${${lib}_SONAME}")
+ endif()
+ endif()
+ endforeach()
+ if(arg_REQUIRED AND NOT ${lib}_SONAME)
+ message(FATAL_ERROR "Could not find dynamically loadable library ${lib}")
+ endif()
+ set(${lib}_SONAME ${${lib}_SONAME} PARENT_SCOPE)
+endfunction()
diff --git a/cmake/scripts/darwin_embedded/PathSetup.cmake b/cmake/scripts/darwin_embedded/PathSetup.cmake
index 32373e3b9f..efaca0bb86 100644
--- a/cmake/scripts/darwin_embedded/PathSetup.cmake
+++ b/cmake/scripts/darwin_embedded/PathSetup.cmake
@@ -1,4 +1,7 @@
set(BUNDLE_IDENTIFIER_DESC "Bundle ID")
+if(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ string(CONCAT BUNDLE_IDENTIFIER_DESC "${BUNDLE_IDENTIFIER_DESC}" " (app, top shelf, group ID)")
+endif()
set(PLATFORM_BUNDLE_IDENTIFIER "${APP_PACKAGE}-${CORE_PLATFORM_NAME_LC}" CACHE STRING "${BUNDLE_IDENTIFIER_DESC}")
list(APPEND final_message "Bundle ID: ${PLATFORM_BUNDLE_IDENTIFIER}")
include(cmake/scripts/osx/PathSetup.cmake)
diff --git a/cmake/treedata/darwin_embedded/ios/ios.txt b/cmake/treedata/darwin_embedded/ios/ios.txt
index c02a0c4ede..f5d13e3a4e 100644
--- a/cmake/treedata/darwin_embedded/ios/ios.txt
+++ b/cmake/treedata/darwin_embedded/ios/ios.txt
@@ -1,4 +1,2 @@
-xbmc/cores/RetroPlayer/process/ios cores/RetroPlayer/process/ios
-xbmc/cores/VideoPlayer/Process/ios cores/VideoPlayer/Process/ios
xbmc/platform/darwin/ios platform/ios
xbmc/windowing/ios windowing/ios
diff --git a/cmake/treedata/darwin_embedded/subdirs.txt b/cmake/treedata/darwin_embedded/subdirs.txt
index 8a60dc490c..d2f500ed15 100644
--- a/cmake/treedata/darwin_embedded/subdirs.txt
+++ b/cmake/treedata/darwin_embedded/subdirs.txt
@@ -1,3 +1,5 @@
+xbmc/cores/RetroPlayer/process/ios cores/RetroPlayer/process/ios
+xbmc/cores/VideoPlayer/Process/ios cores/VideoPlayer/Process/ios
xbmc/input/touch input/touch
xbmc/input/touch/generic input/touch/generic
xbmc/platform/darwin platform/darwin
diff --git a/cmake/treedata/darwin_embedded/tvos/tvos.txt b/cmake/treedata/darwin_embedded/tvos/tvos.txt
new file mode 100755
index 0000000000..d8745c02ff
--- /dev/null
+++ b/cmake/treedata/darwin_embedded/tvos/tvos.txt
@@ -0,0 +1,4 @@
+xbmc/platform/darwin/tvos platform/tvos
+xbmc/platform/darwin/tvos/filesystem platform/darwin/tvos/filesystem
+xbmc/platform/darwin/tvos/input platform/darwin/tvos/input
+xbmc/windowing/tvos windowing/tvos
diff --git a/docs/README.md b/docs/README.md
index 7a3be8f866..004dfd734c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -15,6 +15,7 @@ Kodi uses CMake as its building system but instructions are highly dependent on
<a href="README.iOS.md" title="iOS"><img src="resources/ios.svg" height="84"></a>
<a href="README.Linux.md" title="Linux"><img src="resources/linux.svg" height="84"></a>
<a href="README.macOS.md" title="macOS"><img src="resources/macos.svg" height="84"></a>
+ <a href="README.tvOS.md" title="tvOS"><img src="resources/TvOS.svg" height="84"></a>
<a href="README.openSUSE.md" title="openSUSE"><img src="resources/opensuse.svg" height="84"></a>
<a href="README.RaspberryPi.md" title="Raspberry Pi"><img src="resources/raspberrypi.svg" height="84"></a>
<a href="README.Ubuntu.md" title="Ubuntu"><img src="resources/ubuntu.svg" height="84"></a>
diff --git a/docs/README.tvOS.md b/docs/README.tvOS.md
index 109ade2bec..a2beb42bba 100644
--- a/docs/README.tvOS.md
+++ b/docs/README.tvOS.md
@@ -8,13 +8,21 @@ This guide has been tested with macOS 10.13.4(17E199) High Sierra and 10.14.4(18
2. **[Prerequisites](#2-prerequisites)**
3. **[Get the source code](#3-get-the-source-code)**
4. **[Configure and build tools and dependencies](#4-configure-and-build-tools-and-dependencies)**
-5. **[Build binary add-ons](#5-build-binary-add-ons)**
-6. **[Build Kodi](#6-build-kodi)**
- 6.1. **[Generate Project Files](#61-Generate-Project-Files)**
- 6.2. **[Build with Xcode](#62-build)**
-7. **[Package](#7-package)**
-8. **[Install](#8-install)**
-9. **[Gesture Handling](#9-gesture-handling)**
+5. **[Generate Kodi Build files](#5-Generate-Kodi-Build-files)**
+ 5.1. **[Generate XCode Project Files](#51-Generate-Xcode-Project-Files)**
+ 5.2. **[Build with Xcode](#62-build)**
+6. **[Build Kodi](#6-build-kodi)**
+ 6.1. **[Build with Xcode](#61-Build-with-Xcode)**
+ 6.2. **[Build with xcodebuild](#62-Build-with-xcodebuild)**
+7. **[Packaging to distribute as deb](#7-Packaging-to-distribute-as-deb)**
+ 7.1. **[Package via Xcode](#71-Package-via-Xcode)**
+ 7.2. **[Package via Xcodebuild](#72-Package-via-Xcodebuild)**
+8. **[Signing](#8-Signing)**
+ 8.1. **[Signing using a developer account](#81-Signing-using-a-developer-account)**
+ 8.2. **[Using iOS App Signer to install](#82-Using-iOS-App-Signer-to-install)**
+9. **[Install](#9-Install)**
+ 9.1. **[Jailbroken devices](#91-Jailbroken-devices)**
+ 9.2. **[Using Xcode to install](#92-Using-Xcode-to-install)**
## 1. Document conventions
This guide assumes you are using `terminal`, also known as `console`, `command-line` or simply `cli`. Commands need to be run at the terminal, one at a time and in the provided order.
@@ -54,7 +62,7 @@ Several different strategies are used to draw your attention to certain pieces o
* **[Xcode](https://developer.apple.com/xcode/)**. Install it from the AppStore or from the **[Apple Developer Homepage](https://developer.apple.com/)**.
* Device with **tvOS 11.0 or newer** to install Kodi after build.
-Building for tvOS should work with the following constellations of Xcode and macOS versions:
+Building for tvOS should work with the following combinations of Xcode and macOS versions:
* Xcode 9.x against tvOS SDK 11.x on 10.12.x (Sierra)
* Xcode 9.x against tvOS SDK 11.x on 10.13.x (High Sierra)(recommended)
* Xcode 9.x against tvOS SDK 11.x on 10.14.x (Mojave)(recommended)
@@ -80,9 +88,7 @@ git clone https://github.com/xbmc/xbmc kodi
## 4. Configure and build tools and dependencies
Kodi can be built as a 64bit program only for tvOS. The dependencies are built in `$HOME/kodi/tools/depends` and installed into `/Users/Shared/xbmc-depends`.
-**TIP:** Look for comments starting with `Or ...` and only execute the command(s) you need.
-
-Configure build for 64bit:
+Configure build:
```
cd $HOME/kodi/tools/depends
./bootstrap
@@ -103,140 +109,141 @@ make -j$(getconf _NPROCESSORS_ONLN)
./configure --host=arm-apple-darwin --with-platform=tvos --with-sdk=11.0
```
-**[back to top](#table-of-contents)** | **[back to section top](#4-configure-and-build-tools-and-dependencies)**
+**[back to top](#table-of-contents)**
-## 5. Build binary add-ons
+## 5. Generate Kodi Build files
+Before you can use Xcode to build Kodi, the Xcode project has to be generated with CMake. CMake is built as part of the dependencies and doesn't have to be installed separately. A toolchain file is also generated and is used to configure CMake.
+Default behaviour will not build binary addons. To add addons to your build go to **[Add Binary Addons to Project](#52-Add-Binary-Addons-to-Project)**
-**NOTE:** The below is not currently possible in Kodi master repo
+## 5.1. Generate XCode Project Files
-You can find a complete list of available binary add-ons **[here](https://github.com/xbmc/repo-binary-addons)**.
+Before you can use Xcode to build Kodi, the Xcode project has to be generated with CMake. CMake is built as part of the dependencies and doesn't have to be installed separately. A toolchain file is also generated and is used to configure CMake.
-Change to Kodi's source code directory:
+Create an out-of-source build directory:
```
-cd $HOME/kodi
+mkdir $HOME/kodi-build
```
-Build all add-ons:
+Generate Xcode project for TVOS:
```
-make -C tools/depends/target/binary-addons
+make -C tools/depends/target/cmakebuildsys BUILD_DIR=$HOME/kodi-build
```
-Build specific add-ons:
-```
-make -C tools/depends/target/binary-addons ADDONS="audioencoder.flac pvr.vdr.vnsi audiodecoder.snesapu"
-```
+**TIP:** BUILD_DIR can be omitted, and project will be created in $HOME/kodi/build
+Change all relevant paths onwards if omitted.
-Build a specific group of add-ons:
-```
-make -j$(getconf _NPROCESSORS_ONLN) -C tools/depends/target/binary-addons ADDONS="pvr.*"
-```
+Additional cmake arguments can be supplied via the CMAKE_EXTRA_ARGUMENTS command line variable
-**[back to top](#table-of-contents)**
+An example to set signing settings in xcode project:
+````
+make -C tools/depends/target/cmakebuildsys CMAKE_EXTRA_ARGUMENTS="-DPLATFORM_BUNDLE_IDENTIFIER='tv.kodi.kodi' -DCODE_SIGN_IDENTITY='iPhone Developer: *** (**********)' -DPROVISIONING_PROFILE_APP='tv.kodi.kodi' -DPROVISIONING_PROFILE_TOPSHELF='tv.kodi.kodi.Topshelf'"
+````
+Available Signing arguments
-## 6. Build Kodi
+PLATFORM_BUNDLE_IDENTIFIER - bundle ID (used for the app, top shelf and entitlements)
+DEVELOPMENT_TEAM - dev team ID **OR** CODE_SIGN_IDENTITY - certificate name
+PROVISIONING_PROFILE_APP - provprofile name for the app
+PROVISIONING_PROFILE_TOPSHELF - provprofile name for the top shelf
-**NOTE:** The below is not currently possible in Kodi master repo
+## 5.2. Add Binary Addons to Project
-## 6.1. Generate Project Files
+**TIP:** If you wish to add signing settings automatically, look at **[Generate XCode Project Files](#51-Generate-XCode-Project-Files)** for the additional `CMAKE_EXTRA_ARGUMENTS`
-Before you can use Xcode to build Kodi, the Xcode project has to be generated with CMake. CMake is built as part of the dependencies and doesn't have to be installed separately. A toolchain file is also generated and is used to configure CMake.
+You can find a complete list of available binary add-ons **[here](https://github.com/xbmc/repo-binary-addons)**.
-Create an out-of-source build directory:
+Binary addons will be built as a dependency in the Xcode project. You can choose the addons you wish to build during the Xcode project generation step
+
+Generate Xcode project to build specific add-ons:
```
-mkdir $HOME/kodi-build
+make -C tools/depends/target/cmakebuildsys CMAKE_EXTRA_ARGUMENTS="-DENABLE_XCODE_ADDONBUILD=ON -DADDONS_TO_BUILD='audioencoder.flac pvr.vdr.vnsi audiodecoder.snesapu'"
```
-Generate Xcode project as per configure command in **[Configure and build tools and dependencies](#4-configure-and-build-tools-and-dependencies)**:
+Generate Xcode project to build a specific group of add-ons:
```
-make -C tools/depends/target/cmakebuildsys BUILD_DIR=$HOME/kodi-build
+make -C tools/depends/target/cmakebuildsys CMAKE_EXTRA_ARGUMENTS="-DENABLE_XCODE_ADDONBUILD=ON -DADDONS_TO_BUILD='pvr.*'"
```
-**TIP:** BUILD_DIR can be omitted, and project will be created in $HOME/kodi/build
-Change all relevant paths onwards if omitted.
-
-Additional cmake arguments can be supplied via the CMAKE_EXTRA_ARGUMENTS command line variable
-
-Alternatively:
-`
-Generate Xcode project for ARM 64bit (**recommended**):
+Generate Xcode project to build all add-ons automatically:
```
-/Users/Shared/xbmc-depends/x86_64-darwin18.5.0-native/bin/cmake -G Xcode -DCMAKE_TOOLCHAIN_FILE=/Users/Shared/xbmc-depends/appletvos12.2_arm64-target-debug/share/Toolchain.cmake $HOME/kodi
+make -C tools/depends/target/cmakebuildsys CMAKE_EXTRA_ARGUMENTS="-DENABLE_XCODE_ADDONBUILD=ON"
```
-**WARNING:** The toolchain file location differs depending on your tvOS and SDK version. You have to replace `x86_64-darwin18.5.0-native` and `appletvos12.2_arm64-target-debug` in the paths above with the correct ones on your system.
-
-You can check `Users/Shared/xbmc-depends` directory content with:
+**TIP:** If you wish to not automatically build addons added to your xcode project, omit `-DENABLE_XCODE_ADDONBUILD=ON`. The target will be added to the project, but the dependency will not be set to automatically build
+**TIP:** Binary add-ons added to the generated Xcode project can be built independently of the Kodi app by selecting the scheme/target `binary-addons` in the Xcode project.
+You can also build the binary-addons target via xcodebuild. This will not build the Kodi App, but will build any/all binary addons added for the project Generation.
```
-ls -l /Users/Shared/xbmc-depends
+xcodebuild -config "Debug" -target binary-addons
```
-## 6.2 Build
+**[back to top](#table-of-contents)** | **[back to section top](#5-Generate-Kodi-Build-files)**
+
+## 6. Build
+
+### 6.1. Build with Xcode
-**Start Xcode, open the Kodi project file** (`kodi.xcodeproj`) located in `$HOME/kodi-build` and hit `Build`.
+Start Xcode, open the Kodi project file created in **[Generate Kodi Build files](#5-Generate-Kodi-Build-files)**
-**WARNING:** If you have selected a specific tvOS SDK Version in step 4 then you might need to adapt the active target to use the same tvOS SDK version, otherwise build will fail. Be sure to select a device configuration. Building for simulator is not supported.
+**TIP:** (`kodi.xcodeproj`) is located in `$HOME/kodi-build`
-**Alternatively**, you can also build via Xcode from the command-line with `xcodebuild`:
+Once the project has loaded, select `Generic TvOs Device` (or your actual connected device if you have it connected) and hit `Build`.
-Build Kodi:
+This will create a `Kodi.app` file located in `$HOME/kodi-build/build/Debug-appletvos`. This App can be deployed via Xcode to an AppleTV via `Window -> Devices and Simulators -> Select device and click +`
+
+**TIP:** If you build as a release target, the location of the `Kodi.app` will be `$HOME/kodi-build/build/Release-appletvos`
+
+**WARNING:** If you have selected a specific tvOS SDK Version in step 4 then you might need to adapt the active target to use the same tvOS SDK version, otherwise build will fail. Be sure to select a device configuration.
+**WARNING:** Building for simulator is NOT supported.
+
+### 6.2. Build with xcodebuild
+Alternatively, you can also build via Xcode from the command-line with `xcodebuild`, triggered by CMake:
+
+Change to build directory:
```
cd $HOME/kodi-build
xcodebuild -config "Debug" -jobs $(getconf _NPROCESSORS_ONLN)
```
+This will create a `Kodi.app` file located in `$HOME/kodi-build/build/Debug-appletvos`. This App can be deployed via Xcode to an AppleTV via `Window -> Devices and Simulators -> Select device and click +`
+
**TIP:** You can specify Release instead of Debug as -config parameter.
+**TIP:** If you build as a release target, the location of the `Kodi.app` will be `$HOME/kodi-build/build/Release-appletvos`
-**[back to top](#table-of-contents)** | **[back to section top](#6-build-kodi)**
+**[back to top](#table-of-contents)** | **[back to section top](#6-Build)**
-## 7. Package
+## 7. Packaging to distribute as deb
CMake generates a target called `deb` which will package Kodi ready for distribution. After Kodi has been built, the target can be triggered by selecting it in Xcode active scheme or manually running
-```
-cd $HOME/kodi-build
-xcodebuild -target deb
-```
+## 7.1. Package via Xcode
-**Alternatively**
+Start Xcode, open the Kodi project file created in **[Generate XCode Project Files](#51-Generate-Project-Files)**
-```
-cd $HOME/kodi-build
-/Users/Shared/xbmc-depends/x86_64-darwin18.5.0-native/bin/cmake --build . --target "deb" --config "Debug"
-```
+**TIP:** (`kodi.xcodeproj`) is located in `$HOME/kodi-build`
-The generated package will be located at $HOME/kodi-build/tools/darwin/packaging/tvos.
+Click on `Product` in the top menu bar, and then go to `Scheme`, then select `deb`
-**[back to top](#table-of-contents)**
+Hit `Build`
-## 8. Install
+**TIP:** The generated package will be located at $HOME/kodi-build/tools/darwin/packaging/tvos.
-There are a few different methods that can be used to install kodi on an AppleTV 4/4K.
+## 7.2. Package via Xcodebuild
-### Jailbroken devices
-On jailbroken devices the resulting deb file can be copied to the tvOS device via *ssh/scp* and installed manually. You need to SSH into the tvOS device and issue:
+Change to build directory:
```
-dpkg -i <name of the deb file>
+cd $HOME/kodi-build
+xcodebuild -target deb
```
-### Using Code Signing instead
-
-Whether you have paid or free developer account you can deploy Kodi via Xcode to work on a non-jailbroken devices.
-
-#### Wirelessly connecting to AppleTV 4/4K
-The Apple TV 4K cannot be connected to mac via a cable so the connection must be wireless to XCode to add the application.
+**TIP:** The generated package will be located at $HOME/kodi-build/tools/darwin/packaging/tvos.
- 1. Make sure your Mac and your Apple TV are on the same network.
- 2. Choose `Window->Devices and Simulators`, then in the window that appears, click Devices.
- 3. On your Apple TV, open the Settings app and choose `Remotes and Devices->Remote App and Devices`.
- 4. The Apple TV searches for possible devices including the Mac. (If you have any Firewall or Internet security, disable/turn off to allow searching.)
- 5. On your Mac, select the Apple TV in the Devices pane. The pane for the Apple TV is displayed and shows the current status of the connection request.
- 6. Enter the verification code displayed on your AppleTV into the Device window pane for the device and click Connect.
+**[back to top](#table-of-contents)**
-Xcode sets up the Apple TV for wireless debugging and pairs with the device.
+## 8. Signing
-#### Signing using a paid developer accounts
-For this to work you need to alter the Xcode project by setting your codesign identity.
+**TIP:** If your device is jailbroken, you can go direct to **[Installing on Jailbroken Device](#91-Jailbroken-devices)**
-#### Signing using a free developer account
+## 8.1. Signing using a developer account
+For this to work you need to alter the Xcode project by setting your codesign identity or supplying credentials during
+xcode generation.
Note that using a free developer account the signing will need to be reapplied every 7 days.
1. Open the Xcode project in Xcode as above (requires Xcode 7 or later)
@@ -246,23 +253,53 @@ Note that using a free developer account the signing will need to be reapplied e
3. Under the `General` tab, enter a unique bundle identifer and check the box to `Automatically Manage Signing`.
4. Select your team under `Automatically Manage Signing`.
-#### An important note on Code Signing
+## An important note on Code Signing
It's also important that you select the signing on all 4 spots in the project settings. After the last buildstep, our support script will do a full sign of all binaries and bundle them with the given identity, including all the `*.viz`, `*.pvr`, `*.so`, etc. files Xcode doesn't know anything about. This should allow you to deploy Kodi to all non-jailbroken devices the same way you deploy normal apps to.
In that case Kodi will be sandboxed like any other app. All Kodi files are then located in the sandboxed *Documents* folder and can be easily accessed via iTunes file sharing.
-### Installing on AppleTV
-There are two options for deplying to your AppleTV 4/4K. The first is just by using Run in XCode for a debugging sessions.
+## 8.2. Using iOS App Signer to install
-Note that if you get a App Verification Failed error message when trying to to use `Run` you can delete two files in the created Kodi.app.
+ 1. Build the deb target via xcodebuild or Xcode as per **[Build Kodi](#6-build-kodi)**
+ 2. Open iOS Appsigner
+ 3. Browse to $HOME/kodi/build/tools/darwin/packaging/tvos for your input file
+ 4. Select your signing certificate
+ 5. Select your provisioning profile
+ 6. Click start and select save location for the ipa file
+ 7. Run Xcode -> Window -> Devices and Simulators
+ 8. Select your Apple TV you setup in earlier for Wireless connecting press the +
+ 9. Find your ipa file and click open.
+
+**[back to top](#table-of-contents)**
- * `rm -rf $HOME/kodi-build/build/Debug-appletvos/Kodi.app/_CodeSignature`
- * `rm -f $HOME/kodi-build/build/Debug-appletvos/Kodi.app/embedded.*provision`
+## 9. Install
-The alternative is to deploy the output of the `deb` target. To do this:
+There are a number of different methods that can be used to install kodi on an AppleTV 4/4K.
+
+## 9.1. Jailbroken devices
+On jailbroken devices the resulting deb file created from **[Packaging to distribute as deb](#7-Packaging-to-distribute-as-deb)** can be copied to the tvOS device via *ssh/scp* and installed manually. You need to SSH into the tvOS device and issue:
+```
+dpkg -i <name of the deb file>
+```
+
+## 9.2. Using Xcode to install
+
+Whether you have paid or free developer account you can deploy Kodi via Xcode to work on a non-jailbroken devices.
+
+## Wirelessly connecting to AppleTV 4/4K
+The Apple TV 4K cannot be connected to mac via a cable so the connection must be wireless to XCode to add the application.
+
+ 1. Make sure your Mac and your Apple TV are on the same network.
+ 2. Choose `Window->Devices and Simulators`, then in the window that appears, click Devices.
+ 3. On your Apple TV, open the Settings app and choose `Remotes and Devices->Remote App and Devices`.
+ 4. The Apple TV searches for possible devices including the Mac. (If you have any Firewall or Internet security, disable/turn off to allow searching.)
+ 5. On your Mac, select the Apple TV in the Devices pane. The pane for the Apple TV is displayed and shows the current status of the connection request.
+ 6. Enter the verification code displayed on your AppleTV into the Device window pane for the device and click Connect.
+
+Xcode sets up the Apple TV for wireless debugging and pairs with the device.
+Once your Apple TV has been connected in Xcode, you can deploy either the **[Deb](#7-Packaging-to-distribute-as-deb)** or **[App](#6-Build file)** file.
1. Choose Window > Devices and Simulators, then in the window that appears, click Devices.
2. On your Mac, select the Apple TV in the Devices pane.
3. Click the + symbol under `installed apps` and navigate to and select: `$HOME/kodi-build/build/Debug-appletvos/Kodi.app` and then `Open`.
**[back to top](#table-of-contents)**
-
diff --git a/docs/resources/TvOS.svg b/docs/resources/TvOS.svg
new file mode 100644
index 0000000000..64327f3177
--- /dev/null
+++ b/docs/resources/TvOS.svg
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100pt" height="100pt" viewBox="0 0 100 100" version="1.1">
+<defs>
+<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
+ <feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+</filter>
+<mask id="mask0">
+ <g filter="url(#alpha)">
+<rect x="0" y="0" width="100" height="100" style="fill:rgb(0%,0%,0%);fill-opacity:0.4;stroke:none;"/>
+ </g>
+</mask>
+<image id="image6" width="693" height="408" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArUAAAGYCAYAAABceho0AAAABmJLR0QA/wD/AP+gvaeTAAAQ5UlEQVR4nO3daXccx3mG4QfgAu6kZFNR4liy/P9/U+KFUmJGogiSEAFwgT9UF7umMcRGAPELXNc5fWYwxAzm432Kb1dt5OJtXMLfAADgX9fBRf+BiwjOjcXzjTWvAwBw9R0MjwdrXj835xmaY7xuDo/9+Ri4AABcbQfD9XG6+vP+7+PjF7l5Dp8xBmuP2BvTZ/frRlbjFgCAq2uM2Q9J3g9Xf20M3P6eM/uSqF3G7I0kt6Zra3Hdnv6WsAUAuNrGoH2fZH+6dpPsTc/fTf/2Ybq+OG7PGrU9TG9kjtmtJHeT3EtyP8mD6bo3vX5r+ntmbAEArqZxpOB9Wry+TfJbkp0kb6bnb3M4cvuIwvg5J3aWqF2uzN5JC9eHSR4leZLk8fD4IC1qtzKPIQAAcHX1EYO9tIB9k+RVku0kr6fnr6bXd6bf2cu8epucMmxPu1rag/ZmWqTeSwvZr5L8brp+n+Tr6bUxam9H1AIAXAc9avfTgnUnLWJfpoXti8W1nRa4u9N7+kjCicP2NFHbg7avzj5IC9enSf4tybfT49O0qF2u0vYbxowdAABcbQeZbxDbS4vVN2mrtD1qf07yPMk/pscetzuZV21PHLYnHT9YBu3DtBXZb5P853T9R5JvMgft/el33SQGAHC9rLtZbBxF2E7yS1pPPsl8H1b/n/2+attnbY8N25NE7TJoH01f4A9Jvkvyp7So/TZt/ODh8KVuxXZeAADX0bptvd5lvveqX49yOGrHZjxR2B4XteuC9mla0P4pyQ9Jvk9bpe1B21dnlzEraAEArpcetjcz75Z1J6s7Zt2fno8NubH4jGNHEY6K2vGmsHHkoAftn6fHP0yv96Dtq7OOyAUAuN42Mofo5vS8H9J1Ky1i+5kGy+1fx5Xe8ZjdtWH7uagd96HdSlsSXgbtD5mD9lHmoDVqAABANy5yHmT1BNoeuP153yWr32g2Hs7QX+v/vmJd1I5BezttSfjrJP+eNmrwQ+YV2qeZg9bNYAAAHKU34nJEdezH5Ulk+5n3rx3DdsXnVmr72MHdtGHeb9JuBvt+uo5aoQUAgKOMcXsrqyMHH9JuDttN2y2hH8xw5KljN9b8gY3Mc7SP0/ae/S5t5ODPSf6YFrmPMx9/K2gBADiN5SptH0noIwj98IbdzIcy9BvGPi4/bF3U3kwbOxjnaH9IC9rv0iL3SeagdaACAABntW4kIWnhOq7YjmF7aKZ2jNpxt4O7aaeFfZv5xrDvp5+/yryPWB/0BQCAL7Fcue2ztf3Qht8yh+2h1dp1UXs78/Zdf0xbpR13OniQtiPCctsuAAA4i401z/t87V7a0bk7aWE7HqH7yebiA26kjRTcTRsxeJo2btCPMBtXaAUtAADnZZyr3UrbgetJ2gFfv5ue38+8uLoyLbA5fMh4eti9tBvBvkrbzqt/yPKkMAAAOC/Lw78epHVo79E+MdAPafjUo+tWam+nRe3DtLB9lLmKx71oAQDgvI0LrXczN+njzCfY9ib9ZHPx5jFqH0xvvJ/DhysAAMBFWbfY+iitT5dbym4kh1dqb6atyN6d3jS+8dDsAgAAnLNxLLaPIdzP3Kb94K+VbWU3F28ci/heWtAuTwyzUgsAwEUao3acIrifeeOClfu8PjdTeyctaO8u3gQAAJdhOVt7f7ruZvVmsSTrdz/o23rdzurYgVVaAAAuw3Jnrj4e26cJliu1G8uV2r5au7wELQAAl20cj+1TBHcyR+3aLb2Sw8eTjRcAAFyWU00SfG5OVswCAPD/7cSTBG7+AgDgX9mJJglELQAAFRw5SSBqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAChP1AIAUJ6oBQCgPFELAEB5ohYAgPJELQAA5YlaAADKE7UAAJQnagEAKE/UAgBQnqgFAKA8UQsAQHmiFgCA8kQtAADliVoAAMoTtQAAlCdqAQAoT9QCAFCeqAUAoDxRCwBAeaIWAIDyRC0AAOWJWgAAyhO1AACUJ2oBAKjgYLgO+VzUHvkmAAC4JAdHXJ8so/ZEbwIAgEvQO/TDmmulUTfP8iYAALhgvT8/pvXouyT70+OH6fVPjbp5ljcBAMAl6G26n+TtdO1OP/eF1ySHV2r7m3aHN/Y3fbyELw4AAOOC67vMbfrbdI19epDkYDOHV2n3hzf0Gn4Xq7UAAFyevuDao/ZNkp20Pt1L8j5HrNS+n37p7fSmN5mjdmWJFwAALkhfcH2f1qK9S19n/UrtykztcqX2deYi3p0+9NMbAQDgAvTJgPeZu/RVku20Nv0taxZc183Uvpt++c305leZi7gv8wpbAAAuwtikb9MWWl9O13bme76O3f2g3122PXzA6+m1sYqFLQAA52k5dvA6yYskPyf5JfNi63i/V5LDK7X9DrO+zPvL9CEvpg8dbxoDAIDzMo7D7qWNwP6a5P+SPM8cteNY7Cc3hucbw+NmkptJtpLcSXJvuraS3J7etzn97kYAAODsxqmBvbQx2J+T/JTkL9P10/TaTtas1I5Rm6wP29tpYXsnc9TezGrUClsAAM5qOTHwa5L/TfLXJP+d5O9J/pE2HrubNbtyLaN21GP1Rua43ZquW5nDdnP4fQAAOI3lIQvbaeMGf0sL2r+krdK+yGfmaZP1UbsuTsdV237dmt6/ESu2AACczjhy0G8Me5U2Q/ssc9A+m157nTWHLnSfW6ldt7tBD9tbmVdql7O14/gCAACss+4Y3FdpM7M/psXscuzgbY44N+Go8YPxD/Y39nGEHrPjJWwBADhKb8qPOT5o/5rkf9LGDsabw9Y6Lmr7H+8lPX6Zbl3MLoNW4AIAXE8Hw2M/WGE8LWw7bbygB+1/pQXtT2nbeL1JGzs48qyE424U61/gY1b3DnufxXm7i/cAAMC4OPoh80lhu2mx+mvaTWHPsrpC+2Payu2brO528NnDv24e8yUy/PGdxc97abMNv02Pb5N8neRh2p62481k9rQFALhelkH7PnND7qSt0P6StnXXs7T52WdpIwc/Zz7467NztKOjorZ/mUwf1n8el4x306J2J20W4pu0sH2c5H7a3rbr9rUFAODqWu5ssJ95QfRN5qB9njZm8GNa3D5Pm6HtK7Rrt+9a57io7V8qmbdPGL/g+OVeTl/uaeawfZDkbtretn23BFELAHC1jQuhe5nHDV6nBe2LtNXY52m7G/SY3U5bLO1bd50oaJOTRW3/Ysk8z9CXkceZiJdpQ76/T4var7IatuPxugAAXF29FffTFkD7/+q/zBy147WdeXV2P+vv3TrSaVdN++9vpgVqP2XsXtos7aMkT9Jitj+Oq7WiFgDg6utRO/6v/qu0eH09PX81vb4z/U5fnT32prB1zjoKMB6heyPthrCttHi9lzZP+2C67k2v9wMb7GMLAHA1jdt39RvD+sYCO2kR2zcZ2E0L2f3Ms7N9dfZUQZt8WViO+9P2ldt+2tjW4nKzGADA9bDuJrG+wcAYsX1VdjlqcOqgTc4nLpdx2wP35nDZ1gsA4PpYt51Xv/pr41kIyRljtjvPwBzHCjazGrnLU8cAALjaDrIat+NhXv3fx8cvchGRubF4boYWAOB6Wh6Ru3z93FxGaIpZAIDr7dwjdumf15wp/DIn9PwAAAAASUVORK5CYII="/>
+<clipPath id="clip1">
+ <rect x="0" y="0" width="100" height="100"/>
+</clipPath>
+<g id="surface7" clip-path="url(#clip1)">
+<use xlink:href="#image6" transform="matrix(0.139318,0,0,0.227543,1.753349,3.958401)"/>
+</g>
+<linearGradient id="linear0" gradientUnits="userSpaceOnUse" x1="358.6949" y1="418.2712" x2="358.6949" y2="22.5782" gradientTransform="matrix(0.139318,0,0,0.227543,0.0272365,0)">
+<stop offset="0" style="stop-color:rgb(94.901961%,94.901961%,94.901961%);stop-opacity:1;"/>
+<stop offset="0.5955" style="stop-color:rgb(96.862745%,96.862745%,96.862745%);stop-opacity:1;"/>
+<stop offset="1" style="stop-color:rgb(99.607843%,99.607843%,99.607843%);stop-opacity:1;"/>
+</linearGradient>
+</defs>
+<g id="surface2">
+<use xlink:href="#surface7" mask="url(#mask0)"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:url(#linear0);" d="M 93.921875 5.136719 L 6.078125 5.148438 C 4.183594 5.148438 2.632812 7.679688 2.632812 10.773438 L 2.632812 89.546875 C 2.632812 92.640625 4.183594 95.175781 6.078125 95.175781 L 93.921875 95.167969 C 95.816406 95.167969 97.367188 92.632812 97.367188 89.539062 L 97.367188 10.765625 C 97.367188 7.667969 95.816406 5.136719 93.921875 5.136719 Z M 93.921875 5.136719 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.823529%,18.823529%,18.823529%);fill-opacity:1;" d="M 54.601562 37.304688 C 49.191406 37.304688 45.789062 43.632812 45.789062 53.675781 C 45.789062 63.722656 49.191406 70.03125 54.601562 70.03125 C 60.027344 70.03125 63.429688 63.722656 63.429688 53.675781 C 63.429688 43.632812 60.011719 37.304688 54.601562 37.304688 Z M 54.601562 67.242188 C 50.351562 67.242188 47.671875 61.988281 47.671875 53.679688 C 47.671875 45.351562 50.347656 40.097656 54.601562 40.097656 C 58.855469 40.097656 61.546875 45.351562 61.546875 53.679688 C 61.546875 61.988281 58.851562 67.242188 54.601562 67.242188 Z M 54.601562 67.242188 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.823529%,18.823529%,18.823529%);fill-opacity:1;" d="M 29.4375 69.503906 C 29.25 69.59375 28.496094 69.636719 28.121094 69.636719 C 25.777344 69.636719 24.835938 68.011719 24.835938 63.902344 L 24.835938 49.085938 L 22.75 49.085938 L 22.75 46.667969 L 24.835938 46.667969 L 24.835938 40.957031 L 26.613281 40.957031 L 26.613281 46.667969 L 29.371094 46.667969 L 29.371094 49.085938 L 26.613281 49.085938 L 26.613281 63.746094 C 26.613281 66.121094 27.175781 67.109375 28.46875 67.109375 C 28.65625 67.109375 29.332031 67.0625 29.4375 67.042969 Z M 29.4375 69.503906 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.823529%,18.823529%,18.823529%);fill-opacity:1;" d="M 38.238281 69.527344 L 36.339844 69.527344 L 31.132812 46.667969 L 33.019531 46.667969 L 37.273438 66.472656 L 37.3125 66.472656 L 41.578125 46.667969 L 43.433594 46.667969 Z M 38.238281 69.527344 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.823529%,18.823529%,18.823529%);fill-opacity:1;" d="M 72.460938 70.03125 C 68.398438 70.03125 65.542969 66.359375 65.339844 60.910156 L 67.183594 60.910156 C 67.386719 64.714844 69.582031 67.304688 72.621094 67.304688 C 75.527344 67.304688 77.613281 64.710938 77.613281 61.148438 C 77.613281 58.183594 76.359375 56.421875 73.480469 55.234375 L 71.074219 54.226562 C 67.480469 52.730469 65.878906 50.25 65.878906 46.074219 C 65.878906 40.953125 68.707031 37.304688 72.527344 37.304688 C 76.308594 37.304688 79.082031 40.953125 79.1875 45.984375 L 77.34375 45.984375 C 77.167969 42.40625 75.230469 40.03125 72.472656 40.03125 C 69.78125 40.03125 67.789062 42.46875 67.789062 45.964844 C 67.789062 48.691406 69 50.292969 71.851562 51.480469 L 73.953125 52.359375 C 77.855469 53.964844 79.496094 56.40625 79.496094 60.824219 C 79.5 66.382812 76.753906 70.03125 72.460938 70.03125 Z M 72.460938 70.03125 "/>
+</g>
+</svg>
diff --git a/system/keymaps/customcontroller.SiriRemote.xml b/system/keymaps/customcontroller.SiriRemote.xml
new file mode 100644
index 0000000000..ab0ed63990
--- /dev/null
+++ b/system/keymaps/customcontroller.SiriRemote.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This file contains the mapping of keys (gamepad,remote and keyboard) to actions within XBMC -->
+<!-- The <global> section is a fall through - they will only be used if the button is not -->
+<!-- used in the current window's section. Note that there is only handling -->
+<!-- for a single action per button at this stage. -->
+<!-- For joystick/gamepad configuration under linux/win32, see below as it differs from xbox -->
+<!-- gamepads. -->
+
+<!-- The format is: -->
+<!-- <device> -->
+<!-- <button id=""#>xbmc action</button>
+<!-- </device> -->
+
+<!-- To map keys from other remotes using the RCA protocol, you may add <customcontroller name="SiriRemote"> blocks -->
+<!-- In this case, the tags used are <button id=""#> where # is the original button code (OBC) of the key -->
+<!-- You set it up by adding a <customcontroller name="SiriRemote"> block to the window or <global> section: -->
+<!-- <customcontroller name="SiriRemote"> -->
+<!-- <button id="45">Stop</button>
+<!-- </customcontroller> -->
+
+<!-- Note that the action can be a built-in function. -->
+<!-- eg <button id="6">ActivateWindow(Favourites)</button>
+<!-- would bring up Favourites when the button with the id of 6 is press. In this case, "Menu" -->
+
+<!-- -->
+<!-- Button Ids: -->
+<!-- 'id' is the button ID used by SDL. The key ids recognized from your remote appears -->
+<!-- in /var/log/syslog on the ATV2 for each button pressed and when debug mode is enabled -->
+<!-- Use your log to discover and map custom buttons to actions. -->
+
+<keymap>
+ <global>
+ <customcontroller name="SiriRemote">
+ <!-- up --> <button id="1">Up</button>
+ <!-- down --> <button id="2">Down</button>
+ <!-- left --> <button id="3">Left</button>
+ <!-- right --> <button id="4">Right</button>
+ <!-- center --> <button id="5">Select</button>
+ <!-- menu --> <button id="6">Back</button>
+ <!-- hold center--> <button id="7">ContextMenu</button>
+ <!-- swipe up --> <button id="8">Up</button>
+ <!-- swipe down --> <button id="9">Down</button>
+ <!-- swipe left --> <button id="10">Left</button>
+ <!-- swipe right --> <button id="11">Right</button>
+ <!-- play/pause --> <button id="12">PlayPause</button>
+ <!-- ir play --> <button id="13">Play</button>
+ <!-- ir pause --> <button id="14">Pause</button>
+ <!-- ir stop --> <button id="15">Stop</button>
+ <!-- ir next track --> <button id="16">SkipNext</button>
+ <!-- ir prev track --> <button id="17">SkipPrevious</button>
+ <!-- ir fast forward --> <button id="18">FastForward</button>
+ <!-- ir rewind --> <button id="19">Rewind</button>
+ </customcontroller>
+ </global>
+ <Home>
+ <customcontroller name="SiriRemote">
+ <button id="6">ActivateWindow(Favourites)</button>
+ </customcontroller>
+ </Home>
+ <FullscreenVideo>
+ <customcontroller name="SiriRemote">
+ <button id="1">VolumeUp</button>
+ <button id="2">VolumeDown</button>
+ <button id="3">StepBack</button>
+ <button id="4">StepForward</button>
+ <button id="5">Pause</button>
+ <button id="6">Stop</button>
+ <button id="7">OSD</button>
+ <button id="8">noop</button>
+ <button id="9">noop</button>
+ <button id="10">noop</button>
+ <button id="11">noop</button>
+ </customcontroller>
+ </FullscreenVideo>
+ <FullscreenLiveTV>
+ <customcontroller name="SiriRemote">
+ <button id="3">ChannelDown</button>
+ <button id="4">ChannelUp</button>
+ <button id="8">noop</button>
+ <button id="9">noop</button>
+ <button id="10">noop</button>
+ <button id="11">noop</button>
+ </customcontroller>
+ </FullscreenLiveTV>
+ <FullscreenRadio>
+ <customcontroller name="SiriRemote">
+ <button id="3">ChannelDown</button>
+ <button id="4">ChannelUp</button>
+ </customcontroller>
+ </FullscreenRadio>
+ <Visualisation>
+ <customcontroller name="SiriRemote">
+ <button id="1">VolumeUp</button>
+ <button id="2">VolumeDown</button>
+ <button id="3">SkipPrevious</button>
+ <button id="4">SkipNext</button>
+ <button id="5">Pause</button>
+ <button id="6">Fullscreen</button>
+ <button id="7">OSD</button>
+ </customcontroller>
+ </Visualisation>
+ <SlideShow>
+ <customcontroller name="SiriRemote">
+ <button id="1">ZoomIn</button>
+ <button id="2">ZoomOut</button>
+ <button id="3">PreviousPicture</button>
+ <button id="4">NextPicture</button>
+ <button id="6">Stop</button>
+ <button id="7">Info</button>
+ </customcontroller>
+ </SlideShow>
+ <ScreenCalibration>
+ <customcontroller name="SiriRemote">
+ <button id="5">NextCalibration</button>
+ </customcontroller>
+ </ScreenCalibration>
+ <VideoOSD>
+ <customcontroller name="SiriRemote">
+ <button id="7">Back</button>
+ </customcontroller>
+ </VideoOSD>
+ <VideoMenu>
+ <customcontroller name="SiriRemote">
+ <button id="5">Select</button>
+ <button id="6">Stop</button>
+ <button id="7">OSD</button>
+ </customcontroller>
+ </VideoMenu>
+ <MyVideoLibrary>
+ <customcontroller name="SiriRemote">
+ <button id="7">Info</button>
+ </customcontroller>
+ </MyVideoLibrary>
+ <MyVideoFiles>
+ <customcontroller name="SiriRemote">
+ <button id="7">Info</button>
+ </customcontroller>
+ </MyVideoFiles>
+ <PictureInfo>
+ <customcontroller name="SiriRemote">
+ <button id="3">Left</button>
+ <button id="4">Right</button>
+ </customcontroller>
+ </PictureInfo>
+</keymap>
diff --git a/system/settings/darwin_tvos.xml b/system/settings/darwin_tvos.xml
new file mode 100644
index 0000000000..89b50d78c9
--- /dev/null
+++ b/system/settings/darwin_tvos.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<settings>
+ <section id="interface">
+ <category id="screensaver">
+ <visible>false</visible>
+ <group id="1">
+ <setting id="screensaver.mode">
+ <default></default>
+ </setting>
+ <setting id="screensaver.usemusicvisinstead">
+ <default>false</default>
+ </setting>
+ <setting id="screensaver.usedimonpause">
+ <default>false</default>
+ </setting>
+ </group>
+ </category>
+ <category id="masterlock">
+ <visible>false</visible>
+ </category>
+ </section>
+ <section id="system">
+ <category id="input">
+ <group id="1">
+ <setting id="input.peripherals">
+ <visible>false</visible>
+ </setting>
+ </group>
+ <group id="2">
+ <setting id="input.enablemouse">
+ <default>false</default>
+ <visible>false</visible>
+ </setting>
+ <setting id="input.controllerconfig">
+ <visible>false</visible>
+ </setting>
+ </group>
+ <group id="3">
+ <setting id="input.appleremotemode">
+ <visible>false</visible>
+ </setting>
+ <setting id="input.appleremotesequencetime">
+ <visible>false</visible>
+ </setting>
+ <setting id="input.applesiri" type="boolean" label="24163" help="24153">
+ <level>1</level>
+ <default>false</default>
+ <control type="toggle" />
+ </setting>
+ <setting id="input.applesiritimeoutenabled" type="boolean" label="24160" help="24161">
+ <level>1</level>
+ <default>true</default>
+ <control type="toggle" />
+ </setting>
+ <setting id="input.applesiritimeout" type="integer" label="24154" help="24155">
+ <level>1</level>
+ <default>60</default>
+ <constraints>
+ <options>
+ <option label="24156">30</option>
+ <option label="24157">60</option>
+ <option label="24158">90</option>
+ <option label="24159">120</option>
+ </options>
+ </constraints>
+ <control type="list" format="integer" />
+ <dependencies>
+ <dependency type="enable" setting="input.applesiritimeoutenabled">true</dependency>
+ </dependencies>
+ </setting>
+ <setting id="input.appleusekodikeyboard" type="boolean" label="24162">
+ <level>1</level>
+ <default>false</default>
+ <control type="toggle" />
+ </setting>
+ </group>
+ </category>
+ </section>
+ <section id="player">
+ <category id="videoplayer">
+ <group id="3">
+ <setting id="videoplayer.hqscalers">
+ <visible>false</visible>
+ </setting>
+ </group>
+ </category>
+ <category id="discs">
+ <group id="3" label="620">
+ <visible>false</visible>
+ </group>
+ </category>
+ </section>
+ <section id="services" label="14036" help="36319">
+ <category id="airplay" label="1273" help="36602">
+ <visible>false</visible>
+ </category>
+ </section>
+ <section id="system">
+ <category id="display">
+ <group id="1">
+ <setting id="videoscreen.fakefullscreen">
+ <visible>false</visible>
+ </setting>
+ <setting id="videoscreen.blankdisplays">
+ <visible>false</visible>
+ </setting>
+ <setting id="videoscreen.resolution" type="integer" parent="videoscreen.screen" label="169" help="36352">
+ <visible>false</visible>
+ </setting>
+ <setting id="videoscreen.vsync" type="integer" label="13105" help="36356">
+ <default>2</default> <!-- VSYNC_ALWAYS -->
+ <visible>false</visible>
+ </setting>
+ </group>
+ </category>
+ <category id="audio">
+ <group id="1">
+ <setting id="audiooutput.audiodevice" type="string">
+ <default>Default</default>
+ <visible>false</visible>
+ </setting>
+ </group>
+ <group id="3">
+ <setting id="audiooutput.passthroughdevice" type="string">
+ <default>Default</default>
+ <visible>false</visible>
+ </setting>
+ <setting id="audiooutput.supportdtshdcpudecoding" type="boolean">
+ <default>false</default>
+ <visible>false</visible>
+ </setting>
+ </group>
+ </category>
+ <category id="powermanagement">
+ <group id="1">
+ <visible>false</visible>
+ </group>
+ </category>
+ </section>
+</settings>
diff --git a/tools/buildsteps/tvos/configure-xbmc b/tools/buildsteps/tvos/configure-xbmc
new file mode 100755
index 0000000000..e3c3767b25
--- /dev/null
+++ b/tools/buildsteps/tvos/configure-xbmc
@@ -0,0 +1,5 @@
+WORKSPACE=${WORKSPACE:-$( cd $(dirname $0)/../../.. ; pwd -P )}
+XBMC_PLATFORM_DIR=tvos
+. $WORKSPACE/tools/buildsteps/defaultenv
+
+make -C $WORKSPACE/tools/depends/target/cmakebuildsys
diff --git a/tools/buildsteps/tvos/make-xbmc b/tools/buildsteps/tvos/make-xbmc
new file mode 100755
index 0000000000..c54913516a
--- /dev/null
+++ b/tools/buildsteps/tvos/make-xbmc
@@ -0,0 +1,7 @@
+WORKSPACE=${WORKSPACE:-$( cd $(dirname $0)/../../.. ; pwd -P )}
+XBMC_PLATFORM_DIR=tvos
+. $WORKSPACE/tools/buildsteps/defaultenv
+
+cd $WORKSPACE/build;xcodebuild -target deb -configuration $Configuration build \
+ SDKROOT=appletvos$SDK_VERSION XBMC_DEPENDS_ROOT=$XBMC_DEPENDS_ROOT \
+ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" -CODE_SIGNING_ALLOWED="NO"
diff --git a/tools/buildsteps/tvos/package b/tools/buildsteps/tvos/package
new file mode 100755
index 0000000000..8774eb130a
--- /dev/null
+++ b/tools/buildsteps/tvos/package
@@ -0,0 +1,12 @@
+WORKSPACE=${WORKSPACE:-$( cd $(dirname $0)/../../.. ; pwd -P )}
+XBMC_PLATFORM_DIR=tvos
+. $WORKSPACE/tools/buildsteps/defaultenv
+
+cd $WORKSPACE/build;xcodebuild -target deb
+cd $WORKSPACE/build/tools/darwin/packaging/darwin_embedded/
+
+#rename for upload
+#e.x. kodi-20130314-8c2fb31-Frodo-tvos.deb
+UPLOAD_FILENAME="kodi-$(getBuildRevDateStr)-tvos.deb"
+mkdir $WORKSPACE/tools/darwin/packaging/tvos
+mv *.deb $WORKSPACE/tools/darwin/packaging/tvos/$UPLOAD_FILENAME
diff --git a/tools/darwin/Support/Codesign.command b/tools/darwin/Support/Codesign.command
index 6d2b5f474a..e69f4ba04d 100755
--- a/tools/darwin/Support/Codesign.command
+++ b/tools/darwin/Support/Codesign.command
@@ -3,9 +3,7 @@
set -x
#this is the list of binaries we have to sign for being able to run un-jailbroken
-LIST_BINARY_EXTENSIONS="dylib so"
-
-export CODESIGN_ALLOCATE=`xcodebuild -find codesign_allocate`
+LIST_BINARY_EXTENSIONS="dylib so 0 vis pvr app"
GEN_ENTITLEMENTS="$NATIVEPREFIX/bin/gen_entitlements.py"
IOS11_ENTITLEMENTS="$XBMC_DEPENDS/share/ios11_entitlements.xml"
@@ -25,29 +23,31 @@ if [ ! -f ${GEN_ENTITLEMENTS} ]; then
exit -1
fi
-
if [ "${PLATFORM_NAME}" == "iphoneos" ] || [ "${PLATFORM_NAME}" == "appletvos" ]; then
if [ -f "/Users/Shared/buildslave/keychain_unlock.sh" ]; then
/Users/Shared/buildslave/keychain_unlock.sh
fi
- #do fake sign - needed for jailbroken ios5.1 devices for some reason
- if [ -f ${LDID} ]; then
- find ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/ -name "*.dylib" | xargs ${LDID} -S${IOS11_ENTITLEMENTS}
- find ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/ -name "*.so" | xargs ${LDID} -S${IOS11_ENTITLEMENTS}
- ${LDID} -S${IOS11_ENTITLEMENTS} ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/${EXECUTABLE_NAME}
+ # todo: is this required anymore?
+ if [ "${PLATFORM_NAME}" == "iphoneos" ]; then
+ #do fake sign - needed for jailbroken ios5.1 devices for some reason
+ if [ -f ${LDID} ]; then
+ find ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/ -name "*.dylib" | xargs ${LDID} -S${IOS11_ENTITLEMENTS}
+ find ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/ -name "*.so" | xargs ${LDID} -S${IOS11_ENTITLEMENTS}
+ ${LDID} -S${IOS11_ENTITLEMENTS} ${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/${EXECUTABLE_NAME}
- #repackage python eggs
- EGGS=`find ${CODESIGNING_FOLDER_PATH} -name "*.egg" -type f`
+ #repackage python eggs
+ EGGS=`find ${CODESIGNING_FOLDER_PATH} -name "*.egg" -type f`
for i in $EGGS; do
echo $i
mkdir del
- unzip $i -d del
- find ./del/ -name "*.so" -type f | xargs ${LDID} -S${IOS11_ENTITLEMENTS}
+ unzip -q $i -d del
+ find ./del/ -name "*.so" -type f | xargs ${LDID} -S${IOS11_ENTITLEMENTS}
rm $i
- cd del && zip -r $i ./* && cd ..
+ cd del && zip -qr $i ./* && cd ..
rm -r ./del/
done
+ fi
fi
# pull the CFBundleIdentifier out of the built xxx.app
@@ -58,36 +58,68 @@ if [ "${PLATFORM_NAME}" == "iphoneos" ] || [ "${PLATFORM_NAME}" == "appletvos" ]
echo "CFBundleIdentifier is ${BUNDLEID}"
-
# Prefer the expanded name, if available.
CODE_SIGN_IDENTITY_FOR_ITEMS="${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
- if [ "${CODE_SIGN_IDENTITY_FOR_ITEMS}" = "" ] ; then
- # Fall back to old behavior.
- CODE_SIGN_IDENTITY_FOR_ITEMS="${CODE_SIGN_IDENTITY}"
- fi
- echo "${CODE_SIGN_IDENTITY_FOR_ITEMS}"
+ if [ "${CODE_SIGN_IDENTITY_FOR_ITEMS}" = "" ] ; then
+ # Fall back to old behavior.
+ CODE_SIGN_IDENTITY_FOR_ITEMS="${CODE_SIGN_IDENTITY}"
+ fi
+ echo "${CODE_SIGN_IDENTITY_FOR_ITEMS}"
${GEN_ENTITLEMENTS} "${BUNDLEID}" "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/${EXECUTABLE_NAME}.xcent";
- codesign -v -f -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" --entitlements "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/${EXECUTABLE_NAME}.xcent" "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}/"
+
+ # delete existing codesigning
+ if [ -d "${CODESIGNING_FOLDER_PATH}/_CodeSignature" ]; then
+ rm -r ${CODESIGNING_FOLDER_PATH}/_CodeSignature
+ fi
+ if [ -f "${CODESIGNING_FOLDER_PATH}/embedded.mobileprovision" ]; then
+ rm -f ${CODESIGNING_FOLDER_PATH}/embedded.mobileprovision
+ fi
#if user has set a code_sign_identity different from iPhone Developer we do a real codesign (for deployment on non-jailbroken devices)
- if ! [ -z "${CODE_SIGN_IDENTITY}" ] && [ "${CODE_SIGN_IDENTITY}" == "iPhone Developer" ] && [ "${CODE_SIGN_IDENTITY}" != "Don't Code Sign" ]; then
+ if ! [ -z "${CODE_SIGN_IDENTITY}" ] && echo ${CODE_SIGN_IDENTITY} | grep -cim1 "iPhone Developer" &>/dev/null; then
echo "Doing a full bundle sign using genuine identity ${CODE_SIGN_IDENTITY}"
for binext in $LIST_BINARY_EXTENSIONS
do
- codesign --deep -fvvv -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" -i "${BUNDLEID}" `find ${CODESIGNING_FOLDER_PATH} -name "*.$binext" -type f` ${CODESIGNING_FOLDER_PATH}
+ echo "Signing binary: $binext"
+ # check if at least 1 file with the extension exists to sign, otherwise do nothing
+ FINDOUTPUT=`find ${CODESIGNING_FOLDER_PATH} -name "*.$binext" -type f`
+ if [ `echo $FINDOUTPUT | wc -l` != 0 ]; then
+ for singlefile in $FINDOUTPUT; do
+ codesign -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" -fvvv -i "${BUNDLEID}" "${singlefile}"
+ done
+ fi
done
echo "In case your app crashes with SIG_SIGN check the variable LIST_BINARY_EXTENSIONS in tools/darwin/Support/Codesign.command"
+ for FRAMEWORK_PATH in `find ${CODESIGNING_FOLDER_PATH} -name "*.framework" -type d`
+ do
+ DYLIB_BASENAME=$(basename "${FRAMEWORK_PATH%.framework}")
+ echo "Signing Framework: ${DYLIB_BASENAME}.framework"
+ FRAMEWORKBUNDLEID="${BUNDLEID}.framework.${DYLIB_BASENAME}"
+ codesign -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" -fvvv -i "${FRAMEWORKBUNDLEID}" ${FRAMEWORK_PATH}/${DYLIB_BASENAME}
+ codesign -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" -fvvv -i "${FRAMEWORKBUNDLEID}" ${FRAMEWORK_PATH}
+ done
+
#repackage python eggs
EGGS=`find ${CODESIGNING_FOLDER_PATH} -name "*.egg" -type f`
+ echo "Signing Eggs"
for i in $EGGS; do
echo $i
mkdir del
- unzip $i -d del
- codesign --deep -fvvv -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" -i "${BUNDLEID}" `find ./del/ -name "*.$binext" -type f` ./del/
+ unzip -q $i -d del
+ for binext in $LIST_BINARY_EXTENSIONS
+ do
+ # check if at least 1 file with the extension exists to sign, otherwise do nothing
+ FINDOUTPUT=`find ./del/ -name "*.$binext" -type f`
+ if [ `echo $FINDOUTPUT | wc -l` != 0 ]; then
+ for singlefile in $FINDOUTPUT; do
+ codesign -s "${CODE_SIGN_IDENTITY_FOR_ITEMS}" -fvvv -i "${BUNDLEID}" "${singlefile}"
+ done
+ fi
+ done
rm $i
- cd del && zip -r $i ./* && cd ..
+ cd del && zip -qr $i ./* && cd ..
rm -r ./del/
done
fi
diff --git a/tools/darwin/Support/GenerateMissingImages-tvos.py b/tools/darwin/Support/GenerateMissingImages-tvos.py
new file mode 100755
index 0000000000..cc71376b8e
--- /dev/null
+++ b/tools/darwin/Support/GenerateMissingImages-tvos.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+
+import sys, os, json
+from subprocess import call
+
+assetCatalogPath = sys.argv[1]
+brandAssetsDir = sys.argv[2] + '.brandassets'
+
+def generateImage(contentsRelativeDir, isBaseImage1x, newWidth, newHeight):
+ contentsDir = os.path.join(assetCatalogPath, contentsRelativeDir)
+ if isBaseImage1x:
+ existingImageIndex = 0
+ newImageIndex = 1
+ else:
+ existingImageIndex = 1
+ newImageIndex = 0
+ with open(os.path.join(contentsDir, 'Contents.json')) as jsonFile:
+ jsonContents = json.load(jsonFile)
+ existingImageRelativePath = jsonContents['images'][existingImageIndex]['filename']
+ existingImagePath = os.path.join(contentsDir, existingImageRelativePath)
+ call(['sips', '--resampleHeightWidth', str(newHeight), str(newWidth), existingImagePath, '--out', os.path.join(contentsDir, jsonContents['images'][newImageIndex]['filename'])])
+
+
+generateImage(sys.argv[3] + '.launchimage', True, 3840, 2160)
+generateImage(os.path.join(brandAssetsDir, 'topshelf_wide.imageset'), True, 4640, 1440)
+
+appIconSmall = os.path.join(brandAssetsDir, 'icon.imagestack')
+for i in xrange(1, 5):
+ generateImage(os.path.join(appIconSmall, 'Layer{}.imagestacklayer'.format(i), 'Content.imageset'), False, 400, 240)
diff --git a/tools/darwin/Support/copyframeworks-darwin_embedded.command b/tools/darwin/Support/copyframeworks-darwin_embedded.command
index 199f58c5f8..1818224136 100755
--- a/tools/darwin/Support/copyframeworks-darwin_embedded.command
+++ b/tools/darwin/Support/copyframeworks-darwin_embedded.command
@@ -68,9 +68,15 @@ echo "Package $FULL_PRODUCT_NAME"
echo "Checking $FULL_PRODUCT_NAME for dylib dependencies"
for a in $(otool -L "$TARGET_BINARY" | grep "$EXTERNAL_LIBS\|$DYLIB_NAMEPATH" | awk ' { print $1 } ') ; do
echo " Packaging $a"
- cp -f "$EXTERNAL_LIBS/lib/$(basename $a)" "$TARGET_FRAMEWORKS/"
- chmod u+w "$TARGET_FRAMEWORKS/$(basename $a)"
- install_name_tool -change "$a" "$DYLIB_NAMEPATH/$(basename $a)" "$TARGET_BINARY"
+ # Soft Frameworks strip dylib from path. Explicitly add dylib
+ if ! [ -f "$EXTERNAL_LIBS/lib/$(basename $a)" ]; then
+ DYLIBNAME="$(basename $a).dylib"
+ else
+ DYLIBNAME="$(basename $a)"
+ fi
+ cp -f "$EXTERNAL_LIBS/lib/$DYLIBNAME" "$TARGET_FRAMEWORKS/"
+ chmod u+w "$TARGET_FRAMEWORKS/$DYLIBNAME"
+ install_name_tool -change "$a" "$DYLIB_NAMEPATH/$DYLIBNAME" "$TARGET_BINARY"
done
echo "Package $EXTERNAL_LIBS/lib/python$PYTHON_VERSION"
diff --git a/tools/darwin/Support/copyframeworks-dylibs2frameworks.command b/tools/darwin/Support/copyframeworks-dylibs2frameworks.command
new file mode 100755
index 0000000000..faa5a51a80
--- /dev/null
+++ b/tools/darwin/Support/copyframeworks-dylibs2frameworks.command
@@ -0,0 +1,106 @@
+#!/bin/bash
+
+# Copyright (C) 2015 Team MrMC
+# https://github.com/MrMC
+#
+# 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 2, 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 MrMC; see the file COPYING. If not, see
+# <http://www.gnu.org/licenses/>.
+
+#set -x
+
+TARGET_CONTENTS="${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}"
+TARGET_FRAMEWORKS=$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH
+
+# use the same date/time stamp format for all CFBundleVersions
+BUNDLE_REVISION=$(date -u +%y%m%d.%H%M)
+
+# ios/tvos use different framework plists
+if [ "${PLATFORM_NAME}" == "appletvos" ]; then
+ SEEDFRAMEWORKPLIST="${SRCROOT}/xbmc/platform/darwin/tvos/FrameworkSeed_Info.plist"
+# todo: implement soft frameworks for ios
+#elif [ "$PLATFORM_NAME" == "iphoneos" ]; then
+# SEEDFRAMEWORKPLIST="${SRCROOT}/xbmc/platform/darwin/ios/FrameworkSeed_Info.plist"
+fi
+
+function convert2framework
+{
+ DYLIB="${1}"
+ # typical darwin dylib name format is lib<name>.<version>.dylib
+ DYLIB_BASENAME=$(basename "${DYLIB}")
+ # strip .<version>.dylib
+ DYLIB_LIBBASENAME="${DYLIB_BASENAME%%.[0-9]*}"
+ # make sure .dylib is stripped
+ DYLIB_LIBNAME="${DYLIB_LIBBASENAME%.dylib}"
+
+ # Update main bundle executable to new location of frameworks
+ install_name_tool -change @executable_path/Frameworks/${DYLIB_BASENAME} @executable_path/Frameworks/${DYLIB_LIBNAME}.framework/${DYLIB_LIBNAME} ${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}/${EXECUTABLE_NAME}
+ install_name_tool -add_rpath @executable_path/Frameworks/${DYLIB_LIBNAME}.framework ${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}/${EXECUTABLE_NAME}
+
+ BUNDLEID=`mdls -raw -name kMDItemCFBundleIdentifier ${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}`
+ if [ "${BUNDLEID}" == "(null)" ] ; then
+ BUNDLEID=`/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' ${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}/Info.plist`
+ fi
+
+ FRAMEWORKBUNDLEID="${BUNDLEID}.framework.${DYLIB_LIBNAME}"
+ echo "CFBundleIdentifier is ${FRAMEWORKBUNDLEID}"
+ echo "convert ${DYLIB_BASENAME} to ${DYLIB_LIBNAME}.framework"
+
+ DEST_FRAMEWORK="${TARGET_FRAMEWORKS}/${DYLIB_LIBNAME}.framework"
+ mkdir -p "${DEST_FRAMEWORK}"
+ mkdir -p "${DEST_FRAMEWORK}/Headers"
+ mkdir -p "${DEST_FRAMEWORK}/Modules"
+
+ # framework plists are binary
+ plutil -convert binary1 "${SEEDFRAMEWORKPLIST}" -o "${DEST_FRAMEWORK}/Info.plist"
+ # set real CFBundleName
+ plutil -replace CFBundleName -string "${DYLIB_LIBNAME}" "${DEST_FRAMEWORK}/Info.plist"
+ # set real CFBundleVersion
+ plutil -replace CFBundleVersion -string "${BUNDLE_REVISION}" "${DEST_FRAMEWORK}/Info.plist"
+ # set real CFBundleIdentifier
+ plutil -replace CFBundleIdentifier -string "${FRAMEWORKBUNDLEID}" "${DEST_FRAMEWORK}/Info.plist"
+ # set real CFBundleExecutable
+ plutil -replace CFBundleExecutable -string "${DYLIB_LIBNAME}" "${DEST_FRAMEWORK}/Info.plist"
+ # move it (not copy)
+ mv -f "${DYLIB}" "${DEST_FRAMEWORK}/${DYLIB_LIBNAME}"
+
+ # fixup loader id/paths
+ LC_ID_DYLIB="@rpath/${DYLIB_LIBNAME}.framework/${DYLIB_LIBNAME}"
+ LC_RPATH1="@executable_path/Frameworks/${DYLIB_LIBNAME}.framework"
+ LC_RPATH2="@loader_path/Frameworks/${DYLIB_LIBNAME}.framework"
+ install_name_tool -id "${LC_ID_DYLIB}" "${DEST_FRAMEWORK}/${DYLIB_LIBNAME}"
+ install_name_tool -add_rpath "${LC_RPATH1}" "${DEST_FRAMEWORK}/${DYLIB_LIBNAME}"
+ install_name_tool -add_rpath "${LC_RPATH2}" "${DEST_FRAMEWORK}/${DYLIB_LIBNAME}"
+
+ if [ "$STRIP_INSTALLED_PRODUCT" == "YES" ]; then
+ strip -x "${DEST_FRAMEWORK}/${DYLIB_LIBNAME}"
+ fi
+
+ if [ "$ACTION" == install ]; then
+ # extract the uuid and use it to find the matching bcsymbolmap (needed for crashlog symbolizing)
+ UUID=$(otool -l "${DEST_FRAMEWORK}/${DYLIB_LIBNAME}" | grep uuid | awk '{ print $2}')
+ echo "bcsymbolmap is ${UUID}"
+ if [ -f "${XBMC_DEPENDS}/bcsymbolmaps/${UUID}.bcsymbolmap" ]; then
+ echo "bcsymbolmap is ${UUID}.bcsymbolmap"
+ cp -f "${XBMC_DEPENDS}/bcsymbolmaps/${UUID}.bcsymbolmap" "${BUILT_PRODUCTS_DIR}/"
+ fi
+ fi
+}
+
+# todo: convert ios to soft frameworks as well to remove this if guard
+if [ "$PLATFORM_NAME" == "appletvos" ]; then
+ # loop over all xxx.dylibs in xxx.app/Frameworks
+ for dylib in $(find "${TARGET_FRAMEWORKS}" -name "*.dylib" -type f); do
+ convert2framework "${dylib}"
+ done
+fi
diff --git a/tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh.in b/tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh.in
index fdcb4c557d..23d654f1a5 100644
--- a/tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh.in
+++ b/tools/darwin/packaging/darwin_embedded/mkdeb-darwin_embedded.sh.in
@@ -10,15 +10,19 @@ DIRNAME=`dirname $0`
DSYM_TARGET_DIR=/Users/Shared/xbmc-depends/dSyms
DSYM_FILENAME=@APP_NAME@.app.dSYM
ARM64=false
+PP_DEVICE=iOS
+if [ "@PLATFORM@" == "appletvos" ]; then
+ PP_DEVICE=tvOS
+fi
if [ "${SWITCH#*debug}" != "${SWITCH}" ]; then
- echo "Packaging Debug target for iOS"
- APP="$DIRNAME/../../../../build/Debug-iphoneos/@APP_NAME@.app"
- DSYM="$DIRNAME/../../../../build/Debug-iphoneos/$DSYM_FILENAME"
+ echo "Packaging Debug target for ${PP_DEVICE}"
+ APP="$DIRNAME/../../../../build/Debug-@PLATFORM@/@APP_NAME@.app"
+ DSYM="$DIRNAME/../../../../build/Debug-@PLATFORM@/$DSYM_FILENAME"
elif [ "${SWITCH#*release}" != "${SWITCH}" ]; then
- echo "Packaging Release target for iOS"
- APP="$DIRNAME/../../../../build/Release-iphoneos/@APP_NAME@.app"
- DSYM="$DIRNAME/../../../../build/Release-iphoneos/$DSYM_FILENAME"
+ echo "Packaging Release target for ${PP_DEVICE}"
+ APP="$DIRNAME/../../../../build/Release-@PLATFORM@/@APP_NAME@.app"
+ DSYM="$DIRNAME/../../../../build/Release-@PLATFORM@/$DSYM_FILENAME"
else
echo "You need to specify the build target"
exit 1
@@ -48,7 +52,7 @@ if [ -f "${NATIVEPREFIX}/bin/dpkg-deb" ]; then
export PATH=${bin_path}:${PATH}
fi
-PACKAGE=org.xbmc.@APP_NAME_LC@-ios
+PACKAGE="@PLATFORM_BUNDLE_IDENTIFIER@"
PACKAGE_ARM64="${PACKAGE}64"
VERSION=@APP_VERSION_MAJOR@.@APP_VERSION_MINOR@
@@ -60,10 +64,10 @@ fi
# customize revision string
[ ! -z "$2" ] && REVISION="$2"
-ARCHIVE=${PACKAGE}_${VERSION}-${REVISION}_iphoneos-arm.deb
+ARCHIVE=${PACKAGE}_${VERSION}-${REVISION}_@PLATFORM@-arm.deb
# package identifier for arm64
-$ARM64 && ARCHIVE=${PACKAGE_ARM64}_${VERSION}-${REVISION}_iphoneos-arm.deb
+$ARM64 && ARCHIVE=${PACKAGE_ARM64}_${VERSION}-${REVISION}_@PLATFORM@-arm.deb
SIZE="$(du -s -k ${APP} | awk '{print $1}')"
@@ -75,22 +79,21 @@ rm -rf $DIRNAME/$ARCHIVE
mkdir -p $DIRNAME/$PACKAGE/DEBIAN
if $ARM64; then
echo "Package: $PACKAGE_ARM64" > $DIRNAME/$PACKAGE/DEBIAN/control
- echo "Name: @APP_NAME@-iOS (64-bit)" >> $DIRNAME/$PACKAGE/DEBIAN/control
+ echo "Name: @APP_NAME@-${PP_DEVICE} (64-bit)" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Pre-Depends: cy+cpu.arm64" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Conflicts: $PACKAGE" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Replaces: $PACKAGE" >> $DIRNAME/$PACKAGE/DEBIAN/control
else
echo "Package: $PACKAGE" > $DIRNAME/$PACKAGE/DEBIAN/control
- echo "Name: @APP_NAME@-iOS" >> $DIRNAME/$PACKAGE/DEBIAN/control
+ echo "Name: @APP_NAME@-${PP_DEVICE}" >> $DIRNAME/$PACKAGE/DEBIAN/control
fi
echo "Priority: Extra" >> $DIRNAME/$PACKAGE/DEBIAN/control
-echo "Depends: firmware (>= @CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET@)" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Version: $VERSION-$REVISION" >> $DIRNAME/$PACKAGE/DEBIAN/control
-echo "Architecture: iphoneos-arm" >> $DIRNAME/$PACKAGE/DEBIAN/control
+echo "Architecture: @CMAKE_SYSTEM_NAME@-@CPU@" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Installed-Size: $SIZE" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Description: @APP_NAME@ Entertainment Center for iOS" >> $DIRNAME/$PACKAGE/DEBIAN/control
-echo "Homepage: http://kodi.tv/" >> $DIRNAME/$PACKAGE/DEBIAN/control
-echo "Maintainer: Memphiz" >> $DIRNAME/$PACKAGE/DEBIAN/control
+echo "Homepage: @APP_WEBSITE@" >> $DIRNAME/$PACKAGE/DEBIAN/control
+echo "Maintainer: Memphiz et al." >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Author: Team-@APP_NAME@" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Section: Multimedia" >> $DIRNAME/$PACKAGE/DEBIAN/control
echo "Icon: file:///Applications/@APP_NAME@.app/AppIcon57x57.png" >> $DIRNAME/$PACKAGE/DEBIAN/control
@@ -103,7 +106,9 @@ chmod +x $DIRNAME/$PACKAGE/DEBIAN/prerm
# postinst: nothing for now.
echo "#!/bin/sh" > $DIRNAME/$PACKAGE/DEBIAN/postinst
echo "chown -R mobile:mobile /Applications/@APP_NAME@.app" >> $DIRNAME/$PACKAGE/DEBIAN/postinst
-cat $DIRNAME/migrate_to_kodi.sh >> $DIRNAME/$PACKAGE/DEBIAN/postinst
+if [ "@PLATFORM@" != "appletvos" ]; then
+ cat $DIRNAME/migrate_to_kodi.sh >> $DIRNAME/$PACKAGE/DEBIAN/postinst
+fi
echo "/usr/bin/uicache" >> $DIRNAME/$PACKAGE/DEBIAN/postinst
echo "echo 'finish:respringing ...'" >> $DIRNAME/$PACKAGE/DEBIAN/postinst
chmod +x $DIRNAME/$PACKAGE/DEBIAN/postinst
diff --git a/tools/depends/target/Makefile b/tools/depends/target/Makefile
index b68e3809aa..c2bd06b898 100644
--- a/tools/depends/target/Makefile
+++ b/tools/depends/target/Makefile
@@ -30,6 +30,7 @@ ifeq ($(OS),darwin_embedded)
EXCLUDED_DEPENDS = libcec libusb
ifeq ($(TARGET_PLATFORM),appletvos)
DEPENDS += boblight
+ EXCLUDED_DEPENDS += libshairplay libplist
endif
DEPENDS += iosentitlements
endif
diff --git a/tools/depends/target/pythonmodule-pil/Makefile b/tools/depends/target/pythonmodule-pil/Makefile
index 4afcb31164..07796ae807 100644
--- a/tools/depends/target/pythonmodule-pil/Makefile
+++ b/tools/depends/target/pythonmodule-pil/Makefile
@@ -19,10 +19,11 @@ endif
ifeq (darwin, $(findstring darwin, $(HOST)))
ifeq ($(OS),darwin_embedded)
PYTHON_O=$(abs_top_srcdir)/target/python3/$(PLATFORM)/Programs/python.o
+PILPATH=$(PREFIX)/share/$(APP_NAME)/addons/script.module.pil
endif
#ensure that only our target ldflags are passed to the python build
LDSHARED:=$(CC) -bundle -undefined dynamic_lookup $(LDFLAGS)
-CROSSFLAGS=PYTHONXCPREFIX="$(PREFIX)" CC="$(CC) $(CFLAGS)" CCSHARED="$(CC) $(CFLAGS) $(PYTHON_O)" LDFLAGS="$(LDFLAGS)" PYTHONPATH="$(PREFIX)/lib/python3.7/site-packages/" LDSHARED="$(LDSHARED)"
+CROSSFLAGS=PYTHONXCPREFIX="$(PREFIX)" CC="$(CC) $(CFLAGS)" CCSHARED="$(CC) $(CFLAGS) $(PYTHON_O)" LDFLAGS="$(LDFLAGS)" PYTHONPATH="$(PILPATH):$(PREFIX)/lib/python3.7/site-packages/" LDSHARED="$(LDSHARED)"
endif
LIBDYLIB=$(PLATFORM)/dist/Pillow-$(VERSION)-py3.7-$(OS)-$(CPU).egg
@@ -52,6 +53,11 @@ ifeq ($(OS),android)
cd $(PLATFORM); $(CROSSFLAGS) $(NATIVEPREFIX)/bin/python3 setup.py install --install-lib $(PREFIX)/share/$(APP_NAME)/addons/script.module.pil
cd $(PREFIX)/share/$(APP_NAME)/addons/script.module.pil/lib && unzip -o ../Pillow-*.egg
cd $(PREFIX)/share/$(APP_NAME)/addons/script.module.pil && rm -rf Pillow-*.egg
+else ifeq ($(TARGET_PLATFORM),appletvos)
+ mkdir -p $(PILPATH)/lib
+ cd $(PLATFORM); $(CROSSFLAGS) $(NATIVEPREFIX)/bin/python3 setup.py install --install-lib $(PILPATH)
+ cd $(PILPATH)/lib && unzip -o ../Pillow-*.egg
+ cd $(PILPATH) && rm -rf Pillow-*.egg
else
cd $(PLATFORM); $(CROSSFLAGS) $(NATIVEPREFIX)/bin/python3 setup.py install --prefix=$(PREFIX)
endif
diff --git a/xbmc/Application.h b/xbmc/Application.h
index 3e76ef9f01..09a456461e 100644
--- a/xbmc/Application.h
+++ b/xbmc/Application.h
@@ -395,6 +395,9 @@ protected:
#if defined(TARGET_DARWIN_IOS)
friend class CWinEventsIOS;
#endif
+#if defined(TARGET_DARWIN_TVOS)
+ friend class CWinEventsTVOS;
+#endif
#if defined(TARGET_ANDROID)
friend class CWinEventsAndroid;
#endif
diff --git a/xbmc/GUIInfoManager.cpp b/xbmc/GUIInfoManager.cpp
index b35196033f..fc9037db68 100644
--- a/xbmc/GUIInfoManager.cpp
+++ b/xbmc/GUIInfoManager.cpp
@@ -1191,6 +1191,14 @@ const infomap weather[] = {{ "isfetched", WEATHER_IS_FETCHED },
/// @return **True** if Kodi is running on an IOS device.
/// <p>
/// }
+/// \table_row3{ <b>`System.Platform.TVOS`</b>,
+/// \anchor System_PlatformTVOS
+/// _boolean_,
+/// @return **True** if Kodi is running on a tvOS device.
+/// <p><hr>
+/// @skinning_v19 **[New Boolean Condition]** \link System_PlatformTVOS `System.Platform.TVOS`\endlink
+/// <p>
+/// }
/// \table_row3{ <b>`System.Platform.Darwin`</b>,
/// \anchor System_PlatformDarwin
/// _boolean_,
@@ -9213,6 +9221,8 @@ int CGUIInfoManager::TranslateSingleString(const std::string &strCondition, bool
return SYSTEM_PLATFORM_DARWIN_OSX;
else if (platform == "ios")
return SYSTEM_PLATFORM_DARWIN_IOS;
+ else if (platform == "tvos")
+ return SYSTEM_PLATFORM_DARWIN_TVOS;
else if (platform == "android")
return SYSTEM_PLATFORM_ANDROID;
}
diff --git a/xbmc/Util.cpp b/xbmc/Util.cpp
index 3f79a7ca3b..8f7252708a 100644
--- a/xbmc/Util.cpp
+++ b/xbmc/Util.cpp
@@ -1576,7 +1576,7 @@ void CUtil::InitRandomSeed()
srand(seed);
}
-#ifdef TARGET_POSIX
+#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN_TVOS)
bool CUtil::RunCommandLine(const std::string& cmdLine, bool waitExit)
{
std::vector<std::string> args = StringUtils::Split(cmdLine, ",");
diff --git a/xbmc/Util.h b/xbmc/Util.h
index 94953a1b10..d814150f1c 100644
--- a/xbmc/Util.h
+++ b/xbmc/Util.h
@@ -168,7 +168,7 @@ public:
// return -1 on error, valid range is 1-3999
static int TranslateRomanNumeral(const char* roman_numeral);
-#ifdef TARGET_POSIX
+#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN_TVOS)
//
// Forks to execute a shell command.
//
diff --git a/xbmc/addons/addoninfo/AddonInfoBuilder.cpp b/xbmc/addons/addoninfo/AddonInfoBuilder.cpp
index 233789587d..6057c03717 100644
--- a/xbmc/addons/addoninfo/AddonInfoBuilder.cpp
+++ b/xbmc/addons/addoninfo/AddonInfoBuilder.cpp
@@ -600,7 +600,9 @@ bool CAddonInfoBuilder::PlatformSupportsAddon(const AddonInfoPtr& addon)
#endif
#elif defined(TARGET_WINDOWS_STORE)
"windowsstore",
-#elif defined(TARGET_DARWIN_IOS)
+#elif defined(TARGET_DARWIN_EMBEDDED)
+ "darwin_embedded",
+#if defined(TARGET_DARWIN_IOS)
"ios",
#if defined(__ARM_ARCH_7A__)
"ios-armv7",
@@ -609,6 +611,10 @@ bool CAddonInfoBuilder::PlatformSupportsAddon(const AddonInfoPtr& addon)
#else
#warning no architecture dependant platform tag
#endif
+#elif defined(TARGET_DARWIN_TVOS)
+ "tvos",
+ "tvos-aarch64",
+#endif
#elif defined(TARGET_DARWIN_OSX)
"osx",
#if defined(__x86_64__)
diff --git a/xbmc/cores/AudioEngine/CMakeLists.txt b/xbmc/cores/AudioEngine/CMakeLists.txt
index 5c60ec00ca..3f97e30220 100644
--- a/xbmc/cores/AudioEngine/CMakeLists.txt
+++ b/xbmc/cores/AudioEngine/CMakeLists.txt
@@ -119,10 +119,15 @@ if(CORE_SYSTEM_NAME STREQUAL osx)
endif()
if(CORE_SYSTEM_NAME STREQUAL darwin_embedded)
- list(APPEND SOURCES Sinks/AESinkDARWINIOS.mm
- Sinks/darwin/CoreAudioHelpers.cpp)
- list(APPEND HEADERS Sinks/AESinkDARWINIOS.h
- Sinks/darwin/CoreAudioHelpers.h)
+ list(APPEND SOURCES Sinks/darwin/CoreAudioHelpers.cpp)
+ list(APPEND HEADERS Sinks/darwin/CoreAudioHelpers.h)
+ if(CORE_PLATFORM_NAME_LC STREQUAL ios)
+ list(APPEND SOURCES Sinks/AESinkDARWINIOS.mm)
+ list(APPEND HEADERS Sinks/AESinkDARWINIOS.h)
+ elseif(CORE_PLATFORM_NAME_LC STREQUAL tvos)
+ list(APPEND SOURCES Sinks/AESinkDARWINTVOS.mm)
+ list(APPEND HEADERS Sinks/AESinkDARWINTVOS.h)
+ endif()
endif()
if(CORE_SYSTEM_NAME STREQUAL android)
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h
new file mode 100644
index 0000000000..bef13c8d2e
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "cores/AudioEngine/Interfaces/AESink.h"
+#include "cores/AudioEngine/Utils/AEDeviceInfo.h"
+
+#define DO_440HZ_TONE_TEST 0
+
+#if DO_440HZ_TONE_TEST
+typedef struct
+{
+ float currentPhase;
+ float phaseIncrement;
+} SineWaveGenerator;
+#endif
+
+class AERingBuffer;
+class CAAudioUnitSink;
+
+class CAESinkDARWINTVOS : public IAESink
+{
+public:
+ const char* GetName() override { return "DARWINTVOS"; }
+
+ CAESinkDARWINTVOS();
+ ~CAESinkDARWINTVOS() override = default;
+
+ static void Register();
+ static void EnumerateDevicesEx(AEDeviceInfoList& list, bool force);
+ static IAESink* Create(std::string& device, AEAudioFormat& desiredFormat);
+
+ bool Initialize(AEAudioFormat& format, std::string& device) override;
+ void Deinitialize() override;
+
+ void GetDelay(AEDelayStatus& status) override;
+ double GetCacheTotal() override;
+ unsigned int AddPackets(uint8_t** data, unsigned int frames, unsigned int offset) override;
+ void Drain() override;
+ bool HasVolume() override;
+
+private:
+ static AEDeviceInfoList m_devices;
+ CAEDeviceInfo m_info;
+ AEAudioFormat m_format;
+
+ CAAudioUnitSink* m_audioSink;
+#if DO_440HZ_TONE_TEST
+ SineWaveGenerator m_SineWaveGenerator;
+#endif
+};
diff --git a/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.mm b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.mm
new file mode 100644
index 0000000000..9b1510c53c
--- /dev/null
+++ b/xbmc/cores/AudioEngine/Sinks/AESinkDARWINTVOS.mm
@@ -0,0 +1,895 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "AESinkDARWINTVOS.h"
+
+#include "ServiceBroker.h"
+#include "cores/AudioEngine/AESinkFactory.h"
+#include "cores/AudioEngine/Sinks/darwin/CoreAudioHelpers.h"
+#include "cores/AudioEngine/Utils/AERingBuffer.h"
+#include "cores/AudioEngine/Utils/AEUtil.h"
+#include "threads/Condition.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/WinSystem.h"
+
+#include "platform/darwin/DarwinUtils.h"
+
+#include <sstream>
+
+#import <AVFoundation/AVAudioSession.h>
+#include <AudioToolbox/AudioToolbox.h>
+
+enum CAChannelIndex
+{
+ CAChannel_PCM_6CHAN = 0,
+ CAChannel_PCM_8CHAN = 1,
+ CAChannel_PCM_DD5_1 = 2,
+};
+
+static enum AEChannel CAChannelMap[3][9] = {
+ {AE_CH_FL, AE_CH_FR, AE_CH_LFE, AE_CH_FC, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FR, AE_CH_LFE, AE_CH_FC, AE_CH_SL, AE_CH_SR, AE_CH_BL, AE_CH_BR, AE_CH_NULL},
+ {AE_CH_FL, AE_CH_FC, AE_CH_FR, AE_CH_BL, AE_CH_BR, AE_CH_LFE, AE_CH_NULL},
+};
+
+static std::string getAudioRoute()
+{
+ std::string route;
+ AVAudioSession* myAudioSession = [AVAudioSession sharedInstance];
+ AVAudioSessionRouteDescription* currentRoute = [myAudioSession currentRoute];
+ NSString* output = [[currentRoute.outputs firstObject] portType];
+ if (output)
+ route = [output UTF8String];
+
+ return route;
+}
+
+static void dumpAVAudioSessionProperties()
+{
+ std::string route = getAudioRoute();
+ CLog::Log(LOGNOTICE, "%s audio route = %s", __PRETTY_FUNCTION__,
+ route.empty() ? "NONE" : route.c_str());
+
+ AVAudioSession* mySession = [AVAudioSession sharedInstance];
+
+ CLog::Log(LOGNOTICE, "%s sampleRate %f", __PRETTY_FUNCTION__, [mySession sampleRate]);
+ CLog::Log(LOGNOTICE, "%s outputLatency %f", __PRETTY_FUNCTION__, [mySession outputLatency]);
+ CLog::Log(LOGNOTICE, "%s IOBufferDuration %f", __PRETTY_FUNCTION__, [mySession IOBufferDuration]);
+ CLog::Log(LOGNOTICE, "%s outputNumberOfChannels %ld", __PRETTY_FUNCTION__,
+ static_cast<long>([mySession outputNumberOfChannels]));
+ // maximumOutputNumberOfChannels provides hints to tvOS audio settings
+ // if 2, then audio is set to two channel stereo. iOS return this unless hdmi connected
+ // if 6, then audio is set to Digial Dolby 5.1 OR hdmi path detected sink can only handle 6 channels.
+ // if 8, then audio is set to Best Quality AND hdmi path detected sink can handle 8 channels.
+ CLog::Log(LOGNOTICE, "%s maximumOutputNumberOfChannels %ld", __PRETTY_FUNCTION__,
+ static_cast<long>([mySession maximumOutputNumberOfChannels]));
+
+ //CDarwinUtils::DumpAudioDescriptions(__PRETTY_FUNCTION__);
+}
+
+static bool deactivateAudioSession(int count)
+{
+ if (--count < 0)
+ return false;
+
+ bool rtn = false;
+ NSError* err = nullptr;
+ // deactvivate the session
+ AVAudioSession* mySession = [AVAudioSession sharedInstance];
+ if (![mySession setActive:NO error:&err])
+ {
+ CLog::Log(LOGWARNING, "AVAudioSession setActive NO failed, count %d", count);
+ usleep(10 * 1000);
+ rtn = deactivateAudioSession(count);
+ }
+ else
+ {
+ rtn = true;
+ }
+ return rtn;
+}
+
+static void setAVAudioSessionProperties(NSTimeInterval bufferseconds,
+ double samplerate,
+ int channels)
+{
+ // darwin docs and technotes say,
+ // deavtivate the session before changing the values
+ AVAudioSession* mySession = [AVAudioSession sharedInstance];
+
+ // need to fetch maximumOutputNumberOfChannels when active
+ NSInteger maxchannels = [mySession maximumOutputNumberOfChannels];
+
+ NSError* err = nil;
+ // deactvivate the session
+ if (!deactivateAudioSession(10))
+ CLog::Log(LOGWARNING, "AVAudioSession setActive NO failed: %ld", static_cast<long>(err.code));
+
+ // change the number of channels
+ if (channels > maxchannels)
+ channels = static_cast<UInt32>(maxchannels);
+ err = nil;
+ [mySession setPreferredOutputNumberOfChannels:channels error:&err];
+ if (err != nil)
+ CLog::Log(LOGWARNING, "%s setPreferredOutputNumberOfChannels failed", __PRETTY_FUNCTION__);
+
+ // change the sameple rate
+ err = nil;
+ [mySession setPreferredSampleRate:samplerate error:&err];
+ if (err != nil)
+ CLog::Log(LOGWARNING, "%s setPreferredSampleRate failed", __PRETTY_FUNCTION__);
+
+ // change the i/o buffer duration
+ err = nil;
+ [mySession setPreferredIOBufferDuration:bufferseconds error:&err];
+ if (err != nil)
+ CLog::Log(LOGWARNING, "%s setPreferredIOBufferDuration failed", __PRETTY_FUNCTION__);
+
+ // reactivate the session
+ err = nil;
+ if (![mySession setActive:YES error:&err])
+ CLog::Log(LOGWARNING, "AVAudioSession setActive YES failed: %ld", static_cast<long>(err.code));
+
+ // check that we got the samperate what we asked for
+ if (samplerate != [mySession sampleRate])
+ CLog::Log(LOGWARNING, "sampleRate does not match: asked %f, is %f", samplerate,
+ [mySession sampleRate]);
+
+ // check that we got the number of channels what we asked for
+ if (channels != [mySession outputNumberOfChannels])
+ CLog::Log(LOGWARNING, "number of channels do not match: asked %d, is %ld", channels,
+ static_cast<long>([mySession outputNumberOfChannels]));
+}
+
+#pragma mark - SineWaveGenerator
+/***************************************************************************************/
+/***************************************************************************************/
+#if DO_440HZ_TONE_TEST
+static void SineWaveGeneratorInitWithFrequency(SineWaveGenerator* ctx,
+ double frequency,
+ double samplerate)
+{
+ // Given:
+ // frequency in cycles per second
+ // 2*PI radians per sine wave cycle
+ // sample rate in samples per second
+ //
+ // Then:
+ // cycles radians seconds radians
+ // ------ * ------- * ------- = -------
+ // second cycle sample sample
+ ctx->currentPhase = 0.0;
+ ctx->phaseIncrement = frequency * 2 * M_PI / samplerate;
+}
+
+static int16_t SineWaveGeneratorNextSampleInt16(SineWaveGenerator* ctx)
+{
+ int16_t sample = INT16_MAX * sinf(ctx->currentPhase);
+
+ ctx->currentPhase += ctx->phaseIncrement;
+ // Keep the value between 0 and 2*M_PI
+ while (ctx->currentPhase > 2 * M_PI)
+ ctx->currentPhase -= 2 * M_PI;
+
+ return sample / 4;
+}
+static float SineWaveGeneratorNextSampleFloat(SineWaveGenerator* ctx)
+{
+ float sample = MAXFLOAT * sinf(ctx->currentPhase);
+
+ ctx->currentPhase += ctx->phaseIncrement;
+ // Keep the value between 0 and 2*M_PI
+ while (ctx->currentPhase > 2 * M_PI)
+ ctx->currentPhase -= 2 * M_PI;
+
+ return sample / 4;
+}
+#endif
+
+#pragma mark - CAAudioUnitSink
+/***************************************************************************************/
+/***************************************************************************************/
+class CAAudioUnitSink
+{
+public:
+ CAAudioUnitSink();
+ ~CAAudioUnitSink();
+
+ bool open(AudioStreamBasicDescription outputFormat, size_t buffer_size);
+ bool close();
+ bool activate();
+ bool deactivate();
+ void updatedelay(AEDelayStatus& status);
+ double buffertime();
+ unsigned int sampletrate() { return m_outputFormat.mSampleRate; };
+ unsigned int write(uint8_t* data, unsigned int frames, unsigned int framesize);
+ void drain();
+
+private:
+ bool setupAudio();
+
+ // callbacks
+ static OSStatus renderCallback(void* inRefCon,
+ AudioUnitRenderActionFlags* ioActionFlags,
+ const AudioTimeStamp* inTimeStamp,
+ UInt32 inOutputBusNumber,
+ UInt32 inNumberFrames,
+ AudioBufferList* ioData);
+
+ bool m_setup;
+ bool m_activated;
+ AudioUnit m_audioUnit;
+ AudioStreamBasicDescription m_outputFormat;
+ AERingBuffer* m_buffer;
+
+ Float32 m_totalLatency;
+ Float32 m_inputLatency;
+ Float32 m_outputLatency;
+ Float32 m_bufferDuration;
+
+ unsigned int m_sampleRate;
+ unsigned int m_frameSize;
+
+ std::atomic<bool> m_started;
+
+ CAESpinSection m_render_section;
+ std::atomic<int64_t> m_render_timestamp;
+};
+
+CAAudioUnitSink::CAAudioUnitSink()
+ : m_activated(false), m_buffer(nullptr), m_started(false), m_render_timestamp(0)
+{
+}
+
+CAAudioUnitSink::~CAAudioUnitSink()
+{
+ close();
+}
+
+bool CAAudioUnitSink::open(AudioStreamBasicDescription outputFormat, size_t buffer_size)
+{
+ m_setup = false;
+ m_outputFormat = outputFormat;
+ m_outputLatency = 0.0;
+ m_bufferDuration = 0.0;
+ m_sampleRate = static_cast<unsigned int>(outputFormat.mSampleRate);
+ m_frameSize = outputFormat.mChannelsPerFrame * outputFormat.mBitsPerChannel / 8;
+
+ m_buffer = new AERingBuffer(buffer_size);
+
+ return setupAudio();
+}
+
+bool CAAudioUnitSink::close()
+{
+ deactivate();
+ delete m_buffer;
+ m_buffer = NULL;
+
+ m_started = false;
+ return true;
+}
+
+bool CAAudioUnitSink::activate()
+{
+ if (!m_activated)
+ {
+ if (setupAudio())
+ {
+ AudioOutputUnitStart(m_audioUnit);
+ m_activated = true;
+ }
+ }
+
+ return m_activated;
+}
+
+bool CAAudioUnitSink::deactivate()
+{
+ if (m_activated)
+ {
+ AudioUnitReset(m_audioUnit, kAudioUnitScope_Global, 0);
+
+ // this is a delayed call, the OS will block here
+ // until the autio unit actually is stopped.
+ AudioOutputUnitStop(m_audioUnit);
+
+ // detach the render callback on the unit
+ AURenderCallbackStruct callbackStruct = {0};
+ AudioUnitSetProperty(m_audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
+ 0, &callbackStruct, sizeof(callbackStruct));
+
+ AudioUnitUninitialize(m_audioUnit);
+ AudioComponentInstanceDispose(m_audioUnit), m_audioUnit = nullptr;
+
+ m_setup = false;
+ m_activated = false;
+ }
+
+ return m_activated;
+}
+
+void CAAudioUnitSink::updatedelay(AEDelayStatus& status)
+{
+ // return the number of audio frames in buffer, in seconds
+ // use internal framesize, once written,
+ // bytes in buffer are owned by CAAudioUnitSink.
+ unsigned int size;
+ CAESpinLock lock(m_render_section);
+ do
+ {
+ status.tick = m_render_timestamp;
+ status.delay = 0;
+ if (m_buffer)
+ size = m_buffer->GetReadSize();
+ else
+ size = 0;
+ } while (lock.retry());
+
+ // bytes to seconds
+ status.delay += static_cast<double>(size) / static_cast<double>(m_frameSize) /
+ static_cast<double>(m_sampleRate);
+ // add in hw delay and total latency (in seconds)
+ status.delay += m_totalLatency;
+}
+
+double CAAudioUnitSink::buffertime()
+{
+ // return the number of audio frames for the total buffer size, in seconds
+ // use internal framesize, buffer is owned by CAAudioUnitSink.
+ double buffertime;
+ buffertime =
+ static_cast<double>(m_buffer->GetMaxSize()) / static_cast<double>(m_frameSize * m_sampleRate);
+ return buffertime;
+}
+
+CCriticalSection mutex;
+XbmcThreads::ConditionVariable condVar;
+
+unsigned int CAAudioUnitSink::write(uint8_t* data, unsigned int frames, unsigned int framesize)
+{
+ // use the passed in framesize instead of internal,
+ // writes are relative to AE formats. once written,
+ // CAAudioUnitSink owns them.
+ if (m_buffer->GetWriteSize() < frames * framesize)
+ { // no space to write - wait for a bit
+ CSingleLock lock(mutex);
+ unsigned int timeout = 900 * frames / m_sampleRate;
+ if (!m_started)
+ timeout = 4500;
+
+ // we are using a timer here for beeing sure for timeouts
+ // condvar can be woken spuriously as signaled
+ XbmcThreads::EndTime timer(timeout);
+ condVar.wait(mutex, timeout);
+ if (!m_started && timer.IsTimePast())
+ {
+ CLog::Log(LOGERROR, "%s engine didn't start in %d ms!", __FUNCTION__, timeout);
+ return INT_MAX;
+ }
+ }
+
+ unsigned int write_frames = std::min(frames, m_buffer->GetWriteSize() / framesize);
+ if (write_frames)
+ m_buffer->Write(data, write_frames * framesize);
+
+ return write_frames;
+}
+
+void CAAudioUnitSink::drain()
+{
+ unsigned int bytes = m_buffer->GetReadSize();
+ unsigned int totalBytes = bytes;
+ int maxNumTimeouts = 3;
+ unsigned int timeout = buffertime();
+
+ while (bytes && maxNumTimeouts > 0)
+ {
+ CSingleLock lock(mutex);
+ XbmcThreads::EndTime timer(timeout);
+ condVar.wait(mutex, timeout);
+
+ bytes = m_buffer->GetReadSize();
+ // if we timeout and do not consume bytes,
+ // decrease maxNumTimeouts and try again.
+ if (timer.IsTimePast() && bytes == totalBytes)
+ maxNumTimeouts--;
+ totalBytes = bytes;
+ }
+}
+
+bool CAAudioUnitSink::setupAudio()
+{
+ if (m_setup && m_audioUnit)
+ return true;
+
+ // Audio Unit Setup
+ // Describe a default output unit.
+ AudioComponentDescription description = {};
+ description.componentType = kAudioUnitType_Output;
+ description.componentSubType = kAudioUnitSubType_RemoteIO;
+ description.componentManufacturer = kAudioUnitManufacturer_Apple;
+
+ // Get component
+ AudioComponent component;
+ component = AudioComponentFindNext(nullptr, &description);
+ OSStatus status = AudioComponentInstanceNew(component, &m_audioUnit);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "%s error creating audioUnit (error: %d)", __PRETTY_FUNCTION__,
+ static_cast<int>(status));
+ return false;
+ }
+
+ // set the hw buffer size (in seconds), this affects the number of samples
+ // that get rendered every time the audio callback is fired.
+ double samplerate = m_outputFormat.mSampleRate;
+ int channels = m_outputFormat.mChannelsPerFrame;
+ NSTimeInterval bufferseconds =
+ 1024 * m_outputFormat.mChannelsPerFrame / m_outputFormat.mSampleRate;
+ CLog::Log(LOGNOTICE, "%s setting channels %d", __PRETTY_FUNCTION__, channels);
+ CLog::Log(LOGNOTICE, "%s setting samplerate %f", __PRETTY_FUNCTION__, samplerate);
+ CLog::Log(LOGNOTICE, "%s setting buffer duration to %f", __PRETTY_FUNCTION__, bufferseconds);
+ setAVAudioSessionProperties(bufferseconds, samplerate, channels);
+
+ // Get the real output samplerate, the requested might not avaliable
+ Float64 realisedSampleRate = [[AVAudioSession sharedInstance] sampleRate];
+ if (m_outputFormat.mSampleRate != realisedSampleRate)
+ {
+ CLog::Log(LOGNOTICE,
+ "%s couldn't set requested samplerate %d, AudioUnit will resample to %d instead",
+ __PRETTY_FUNCTION__, static_cast<int>(m_outputFormat.mSampleRate),
+ static_cast<int>(realisedSampleRate));
+ // if we don't want AudioUnit to resample - but instead let activeae resample -
+ // reflect the realised samplerate to the output format here
+ // well maybe it is handy in the future - as of writing this
+ // AudioUnit was about 6 times faster then activeae ;)
+ //m_outputFormat.mSampleRate = realisedSampleRate;
+ //m_sampleRate = realisedSampleRate;
+ }
+
+ // Set the output stream format
+ UInt32 ioDataSize = sizeof(AudioStreamBasicDescription);
+ status = AudioUnitSetProperty(m_audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
+ 0, &m_outputFormat, ioDataSize);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "%s error setting stream format on audioUnit (error: %d)",
+ __PRETTY_FUNCTION__, static_cast<int>(status));
+ return false;
+ }
+
+ // Attach a render callback on the unit
+ AURenderCallbackStruct callbackStruct = {0};
+ callbackStruct.inputProc = renderCallback;
+ callbackStruct.inputProcRefCon = this;
+ status = AudioUnitSetProperty(m_audioUnit, kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Input, 0, &callbackStruct, sizeof(callbackStruct));
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "%s error setting render callback for AudioUnit (error: %d)",
+ __PRETTY_FUNCTION__, static_cast<int>(status));
+ return false;
+ }
+
+ status = AudioUnitInitialize(m_audioUnit);
+ if (status != noErr)
+ {
+ CLog::Log(LOGERROR, "%s error initializing AudioUnit (error: %d)", __PRETTY_FUNCTION__,
+ static_cast<int>(status));
+ return false;
+ }
+
+ AVAudioSession* mySession = [AVAudioSession sharedInstance];
+ m_inputLatency = [mySession inputLatency];
+ m_outputLatency = [mySession outputLatency];
+ m_bufferDuration = [mySession IOBufferDuration];
+ m_totalLatency = m_outputLatency + m_bufferDuration;
+ CLog::Log(LOGNOTICE, "%s total latency = %f", __PRETTY_FUNCTION__, m_totalLatency);
+
+ m_setup = true;
+ std::string formatString;
+ CLog::Log(LOGNOTICE, "%s setup audio format: %s", __PRETTY_FUNCTION__,
+ StreamDescriptionToString(m_outputFormat, formatString));
+
+ dumpAVAudioSessionProperties();
+
+ return m_setup;
+}
+
+inline void LogLevel(unsigned int got, unsigned int wanted)
+{
+ static unsigned int lastReported = INT_MAX;
+ if (got != wanted)
+ {
+ if (got != lastReported)
+ {
+ CLog::Log(LOGWARNING, "DARWINIOS: %sflow (%u vs %u bytes)", got > wanted ? "over" : "under",
+ got, wanted);
+ lastReported = got;
+ }
+ }
+ else
+ lastReported = INT_MAX; // indicate we were good at least once
+}
+
+OSStatus CAAudioUnitSink::renderCallback(void* inRefCon,
+ AudioUnitRenderActionFlags* ioActionFlags,
+ const AudioTimeStamp* inTimeStamp,
+ UInt32 inOutputBusNumber,
+ UInt32 inNumberFrames,
+ AudioBufferList* ioData)
+{
+ CAAudioUnitSink* sink = (CAAudioUnitSink*)inRefCon;
+
+ sink->m_render_section.enter();
+ sink->m_started = true;
+
+ for (unsigned int i = 0; i < ioData->mNumberBuffers; i++)
+ {
+ unsigned int wanted = ioData->mBuffers[i].mDataByteSize;
+ unsigned int bytes = std::min(sink->m_buffer->GetReadSize(), wanted);
+ sink->m_buffer->Read(static_cast<unsigned char*>(ioData->mBuffers[i].mData), bytes);
+ LogLevel(bytes, wanted);
+
+ if (bytes == 0)
+ {
+ // Apple iOS docs say kAudioUnitRenderAction_OutputIsSilence provides a hint to
+ // the audio unit that there is no audio to process. and you must also explicitly
+ // set the buffers contents pointed at by the ioData parameter to 0.
+ memset(ioData->mBuffers[i].mData, 0x00, ioData->mBuffers[i].mDataByteSize);
+ *ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
+ }
+ else if (bytes < wanted)
+ {
+ // zero out what we did not copy over (underflow)
+ uint8_t* empty = static_cast<uint8_t*>(ioData->mBuffers[i].mData) + bytes;
+ memset(empty, 0x00, wanted - bytes);
+ }
+ }
+
+ sink->m_render_timestamp = inTimeStamp->mHostTime;
+ sink->m_render_section.leave();
+ // tell the sink we're good for more data
+ condVar.notifyAll();
+
+ return noErr;
+}
+
+#pragma mark - EnumerateDevices
+/***************************************************************************************/
+/***************************************************************************************/
+static void EnumerateDevices(AEDeviceInfoList& list)
+{
+ CAEDeviceInfo device;
+
+ device.m_deviceName = "default";
+ device.m_displayName = "Default";
+ device.m_displayNameExtra = "";
+
+ // if not hdmi, CAESinkDARWINIOS::Initialize will kick back to 2 channel PCM
+ device.m_deviceType = AE_DEVTYPE_HDMI;
+ device.m_wantsIECPassthrough = true;
+
+ // Passthrough only working < tvos 11.2??
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_AC3);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_EAC3);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_512);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_1024);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTS_2048);
+ device.m_streamTypes.push_back(CAEStreamInfo::STREAM_TYPE_DTSHD_CORE);
+
+ device.m_sampleRates.push_back(44100);
+ device.m_sampleRates.push_back(48000);
+
+ device.m_dataFormats.push_back(AE_FMT_RAW);
+ device.m_dataFormats.push_back(AE_FMT_S16LE);
+ device.m_dataFormats.push_back(AE_FMT_FLOAT);
+
+ // add channel info
+ NSInteger maxChannels = [[AVAudioSession sharedInstance] maximumOutputNumberOfChannels];
+ if (maxChannels > 6)
+ device.m_channels = AE_CH_LAYOUT_7_1;
+ else
+ device.m_channels = AE_CH_LAYOUT_5_1;
+
+ CLog::Log(LOGDEBUG, "EnumerateDevices:Device(%s)", device.m_deviceName.c_str());
+
+ list.push_back(device);
+}
+
+#pragma mark - AEDeviceInfoList
+/***************************************************************************************/
+/***************************************************************************************/
+AEDeviceInfoList CAESinkDARWINTVOS::m_devices;
+
+CAESinkDARWINTVOS::CAESinkDARWINTVOS() : m_audioSink(nullptr)
+{
+}
+
+void CAESinkDARWINTVOS::Register()
+{
+ AE::AESinkRegEntry reg;
+ reg.sinkName = "DARWINTVOS";
+ reg.createFunc = CAESinkDARWINTVOS::Create;
+ reg.enumerateFunc = CAESinkDARWINTVOS::EnumerateDevicesEx;
+ AE::CAESinkFactory::RegisterSink(reg);
+}
+
+IAESink* CAESinkDARWINTVOS::Create(std::string& device, AEAudioFormat& desiredFormat)
+{
+ IAESink* sink = new CAESinkDARWINTVOS();
+ if (sink->Initialize(desiredFormat, device))
+ return sink;
+
+ delete sink;
+ return nullptr;
+}
+
+bool CAESinkDARWINTVOS::Initialize(AEAudioFormat& format, std::string& device)
+{
+ std::string route = getAudioRoute();
+ // no route, no audio. bail and let AE kick back to NULL device
+ if (route.empty())
+ return false;
+
+ // no device, bail and let AE kick back to NULL device
+ bool found = false;
+ std::string devicelower = device;
+ StringUtils::ToLower(devicelower);
+ for (size_t i = 0; i < m_devices.size(); i++)
+ {
+ if (devicelower.find(m_devices[i].m_deviceName) != std::string::npos)
+ {
+ m_info = m_devices[i];
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return false;
+
+ AudioStreamBasicDescription audioFormat = {0};
+ audioFormat.mFormatID = kAudioFormatLinearPCM;
+
+ // check if are we dealing with raw formats or pcm
+ bool passthrough = false;
+ switch (format.m_dataFormat)
+ {
+ case AE_FMT_RAW:
+ // this will be selected when AE wants AC3 or DTS or anything other then float
+ format.m_dataFormat = AE_FMT_S16LE;
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsSignedInteger;
+ if (route.find("HDMI") != std::string::npos)
+ passthrough = true;
+ else
+ {
+ // this should never happen but we cover it just in case
+ // for iOS/tvOS, if we are not hdmi, we cannot do raw
+ // so kick back to pcm.
+ format.m_dataFormat = AE_FMT_FLOAT;
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsFloat;
+ }
+ break;
+ default:
+ // AE lies, even when we register formats we can handle,
+ // it shoves everything down and it is up to the sink
+ // to check/verify and kick back to what the sink supports
+ format.m_dataFormat = AE_FMT_FLOAT;
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsFloat;
+ break;
+ }
+
+ // check and correct sample rates to what we support,
+ // remember, AE is a lier and we need to check/verify
+ // and kick back to what the sink supports
+ switch (format.m_sampleRate)
+ {
+ case 11025:
+ case 22050:
+ case 44100:
+ case 88200:
+ case 176400:
+ if (route.find("HDMI") != std::string::npos)
+ audioFormat.mSampleRate = 48000;
+ else
+ audioFormat.mSampleRate = 44100;
+ break;
+ default:
+ case 8000:
+ case 12000:
+ case 16000:
+ case 24000:
+ case 32000:
+ case 48000:
+ case 96000:
+ case 192000:
+ case 384000:
+ audioFormat.mSampleRate = 48000;
+ break;
+ }
+
+ if (passthrough)
+ {
+ // passthrough is special, PCM encapsulated IEC61937 packets.
+ // make sure input and output samplerate match for preventing resampling
+ audioFormat.mSampleRate = [[AVAudioSession sharedInstance] sampleRate];
+ audioFormat.mFramesPerPacket = 1; // must be 1
+ audioFormat.mChannelsPerFrame = 2; // passthrough needs 2 channels
+ audioFormat.mBitsPerChannel = 16;
+ audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * (audioFormat.mBitsPerChannel >> 3);
+ audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame * audioFormat.mFramesPerPacket;
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsPacked;
+ }
+ else
+ {
+ NSInteger maxChannels = [[AVAudioSession sharedInstance] maximumOutputNumberOfChannels];
+ audioFormat.mFramesPerPacket = 1; // must be 1
+
+ // tvos supports up to 8 channels
+ audioFormat.mChannelsPerFrame = format.m_channelLayout.Count();
+ // clamp number of channels to what tvOS reports
+ if (maxChannels == 2)
+ audioFormat.mChannelsPerFrame = (UInt32)maxChannels;
+
+ audioFormat.mBitsPerChannel = CAEUtil::DataFormatToBits(format.m_dataFormat);
+ audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * (audioFormat.mBitsPerChannel >> 3);
+ audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame * audioFormat.mFramesPerPacket;
+ audioFormat.mFormatFlags |= kLinearPCMFormatFlagIsPacked;
+
+ CAEChannelInfo channel_info;
+ CAChannelIndex channel_index = CAChannel_PCM_6CHAN;
+ if (maxChannels == 6 && format.m_channelLayout.Count() == 6)
+ {
+ // if 6, then audio is set to Digial Dolby 5.1, need to use DD mapping
+ channel_index = CAChannel_PCM_DD5_1;
+ }
+ else if (format.m_channelLayout.Count() == 5)
+ {
+ // if 5, then audio is set to Digial Dolby 5.0, need to use DD mapping
+ channel_index = CAChannel_PCM_DD5_1;
+ }
+ else
+ {
+ if (format.m_channelLayout.Count() > 6)
+ channel_index = CAChannel_PCM_8CHAN;
+ }
+ for (size_t chan = 0; chan < format.m_channelLayout.Count(); ++chan)
+ {
+ if (chan < maxChannels)
+ channel_info += CAChannelMap[channel_index][chan];
+ }
+ format.m_channelLayout = channel_info;
+ }
+
+ std::string formatString;
+ CLog::Log(LOGDEBUG, "%s: AudioStreamBasicDescription: %s %s", __PRETTY_FUNCTION__,
+ StreamDescriptionToString(audioFormat, formatString),
+ passthrough ? "passthrough" : "pcm");
+
+#if DO_440HZ_TONE_TEST
+ SineWaveGeneratorInitWithFrequency(&m_SineWaveGenerator, 440.0, audioFormat.mSampleRate);
+#endif
+
+ size_t buffer_size;
+ switch (format.m_streamInfo.m_type)
+ {
+ case CAEStreamInfo::STREAM_TYPE_AC3:
+ if (!format.m_streamInfo.m_ac3FrameSize)
+ format.m_streamInfo.m_ac3FrameSize = 1536;
+ format.m_frames = format.m_streamInfo.m_ac3FrameSize;
+ buffer_size = format.m_frames * 8;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_EAC3:
+ if (!format.m_streamInfo.m_ac3FrameSize)
+ format.m_streamInfo.m_ac3FrameSize = 1536;
+ format.m_frames = format.m_streamInfo.m_ac3FrameSize;
+ buffer_size = format.m_frames * 8;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTS_512:
+ case CAEStreamInfo::STREAM_TYPE_DTSHD_CORE:
+ format.m_frames = 512;
+ buffer_size = 16384;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTS_1024:
+ format.m_frames = 1024;
+ buffer_size = 16384;
+ break;
+ case CAEStreamInfo::STREAM_TYPE_DTS_2048:
+ format.m_frames = 2048;
+ buffer_size = 16384;
+ break;
+ default:
+ format.m_frames = 1024;
+ buffer_size = (512 * audioFormat.mBytesPerFrame) * 8;
+ break;
+ }
+ m_audioSink = new CAAudioUnitSink;
+ m_audioSink->open(audioFormat, buffer_size);
+ // reset to the realised samplerate
+ format.m_sampleRate = m_audioSink->sampletrate();
+ format.m_frameSize =
+ format.m_channelLayout.Count() * (CAEUtil::DataFormatToBits(format.m_dataFormat) >> 3);
+
+ m_format = format;
+
+ if (!m_audioSink->activate())
+ return false;
+
+ return true;
+}
+
+void CAESinkDARWINTVOS::Deinitialize()
+{
+ delete m_audioSink;
+ m_audioSink = nullptr;
+}
+
+void CAESinkDARWINTVOS::GetDelay(AEDelayStatus& status)
+{
+ if (m_audioSink)
+ m_audioSink->updatedelay(status);
+ else
+ status.SetDelay(0.0);
+}
+
+double CAESinkDARWINTVOS::GetCacheTotal()
+{
+ if (m_audioSink)
+ return m_audioSink->buffertime();
+ return 0.0;
+}
+
+unsigned int CAESinkDARWINTVOS::AddPackets(uint8_t** data, unsigned int frames, unsigned int offset)
+{
+ uint8_t* buffer = data[0] + (offset * m_format.m_frameSize);
+#if DO_440HZ_TONE_TEST
+ if (m_format.m_dataFormat == AE_FMT_FLOAT)
+ {
+ float* samples = static_cast<float*>(buffer);
+ for (unsigned int j = 0; j < frames; j++)
+ {
+ float sample = SineWaveGeneratorNextSampleFloat(&m_SineWaveGenerator);
+ *samples++ = sample;
+ *samples++ = sample;
+ }
+ }
+ else
+ {
+ int16_t* samples = (int16_t*)buffer;
+ for (unsigned int j = 0; j < frames; j++)
+ {
+ int16_t sample = SineWaveGeneratorNextSampleInt16(&m_SineWaveGenerator);
+ *samples++ = sample;
+ *samples++ = sample;
+ }
+ }
+#endif
+ if (m_audioSink)
+ return m_audioSink->write(buffer, frames, m_format.m_frameSize);
+ return 0;
+}
+
+void CAESinkDARWINTVOS::Drain()
+{
+ if (m_audioSink)
+ m_audioSink->drain();
+}
+
+bool CAESinkDARWINTVOS::HasVolume()
+{
+ return false;
+}
+
+void CAESinkDARWINTVOS::EnumerateDevicesEx(AEDeviceInfoList& list, bool force)
+{
+ m_devices.clear();
+ EnumerateDevices(m_devices);
+ list = m_devices;
+}
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt
index c6150b2878..12b67ebd48 100644
--- a/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/CMakeLists.txt
@@ -42,6 +42,7 @@ endif()
if(OPENGLES_FOUND AND (CORE_PLATFORM_NAME_LC STREQUAL android OR
CORE_PLATFORM_NAME_LC STREQUAL ios OR
+ CORE_PLATFORM_NAME_LC STREQUAL tvos OR
CORE_PLATFORM_NAME_LC STREQUAL gbm OR
CORE_PLATFORM_NAME_LC STREQUAL x11 OR
CORE_PLATFORM_NAME_LC STREQUAL wayland))
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp
index 41b60c0d5d..91ef47fee1 100644
--- a/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.cpp
@@ -15,7 +15,13 @@
#include "settings/MediaSettings.h"
#include "utils/GLUtils.h"
#include "utils/log.h"
+#if defined(TARGET_DARWIN_IOS)
#include "windowing/ios/WinSystemIOS.h"
+#define WIN_SYSTEM_CLASS CWinSystemIOS
+#elif defined(TARGET_DARWIN_TVOS)
+#include "windowing/tvos/WinSystemTVOS.h"
+#define WIN_SYSTEM_CLASS CWinSystemTVOS
+#endif
#include <CoreVideo/CVBuffer.h>
#include <CoreVideo/CVPixelBuffer.h>
@@ -39,7 +45,7 @@ bool CRendererVTB::Register()
CRendererVTB::CRendererVTB()
{
m_textureCache = nullptr;
- CWinSystemIOS* winSystem = dynamic_cast<CWinSystemIOS*>(CServiceBroker::GetWinSystem());
+ auto winSystem = dynamic_cast<WIN_SYSTEM_CLASS*>(CServiceBroker::GetWinSystem());
m_glContext = winSystem->GetEAGLContextObj();
CVReturn ret = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault,
NULL,
diff --git a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt
index bf48f39829..a5292d87d4 100644
--- a/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt
+++ b/xbmc/cores/VideoPlayer/VideoRenderers/VideoShaders/CMakeLists.txt
@@ -24,6 +24,7 @@ endif()
if(OPENGLES_FOUND AND (CORE_PLATFORM_NAME_LC STREQUAL android OR
CORE_PLATFORM_NAME_LC STREQUAL ios OR
+ CORE_PLATFORM_NAME_LC STREQUAL tvos OR
CORE_PLATFORM_NAME_LC STREQUAL gbm OR
CORE_PLATFORM_NAME_LC STREQUAL x11 OR
CORE_PLATFORM_NAME_LC STREQUAL wayland))
diff --git a/xbmc/filesystem/DirectoryFactory.cpp b/xbmc/filesystem/DirectoryFactory.cpp
index 134755b908..0004252bce 100644
--- a/xbmc/filesystem/DirectoryFactory.cpp
+++ b/xbmc/filesystem/DirectoryFactory.cpp
@@ -53,6 +53,8 @@
#include "PVRDirectory.h"
#if defined(TARGET_ANDROID)
#include "platform/android/filesystem/APKDirectory.h"
+#elif defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/filesystem/TVOSDirectory.h"
#endif
#include "XbtDirectory.h"
#include "ZipDirectory.h"
@@ -107,7 +109,14 @@ IDirectory* CDirectoryFactory::Create(const CURL& url)
}
#ifdef TARGET_POSIX
- if (url.GetProtocol().empty() || url.IsProtocol("file")) return new CPosixDirectory();
+ if (url.GetProtocol().empty() || url.IsProtocol("file"))
+ {
+#if defined(TARGET_DARWIN_TVOS)
+ if (CTVOSDirectory::WantsDirectory(url))
+ return new CTVOSDirectory();
+#endif
+ return new CPosixDirectory();
+ }
#elif defined(TARGET_WINDOWS)
if (url.GetProtocol().empty() || url.IsProtocol("file")) return new CWin32Directory();
#else
diff --git a/xbmc/filesystem/FileFactory.cpp b/xbmc/filesystem/FileFactory.cpp
index 7e70fff481..92788507f1 100644
--- a/xbmc/filesystem/FileFactory.cpp
+++ b/xbmc/filesystem/FileFactory.cpp
@@ -39,6 +39,9 @@
#if defined(TARGET_ANDROID)
#include "platform/android/filesystem/AndroidAppFile.h"
#endif
+#if defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/filesystem/TVOSFile.h"
+#endif // TARGET_DARWIN_TVOS
#ifdef HAS_UPNP
#include "UPnPFile.h"
#endif
@@ -102,7 +105,14 @@ IFile* CFileFactory::CreateLoader(const CURL& url)
else if (url.IsProtocol("multipath")) return new CMultiPathFile();
else if (url.IsProtocol("image")) return new CImageFile();
#ifdef TARGET_POSIX
- else if (url.IsProtocol("file") || url.GetProtocol().empty()) return new CPosixFile();
+ else if (url.IsProtocol("file") || url.GetProtocol().empty())
+ {
+#if defined(TARGET_DARWIN_TVOS)
+ if (CTVOSFile::WantsFile(url))
+ return new CTVOSFile();
+#endif
+ return new CPosixFile();
+ }
#elif defined(TARGET_WINDOWS)
else if (url.IsProtocol("file") || url.GetProtocol().empty())
{
diff --git a/xbmc/guilib/GUIKeyboardFactory.cpp b/xbmc/guilib/GUIKeyboardFactory.cpp
index 0a09cc5d56..3289999648 100644
--- a/xbmc/guilib/GUIKeyboardFactory.cpp
+++ b/xbmc/guilib/GUIKeyboardFactory.cpp
@@ -75,7 +75,6 @@ bool CGUIKeyboardFactory::SendTextToActiveKeyboard(const std::string &aTextStrin
bool CGUIKeyboardFactory::ShowAndGetInput(std::string& aTextString, CVariant heading, bool allowEmptyResult, bool hiddenInput /* = false */, unsigned int autoCloseMs /* = 0 */)
{
bool confirmed = false;
- CGUIKeyboard *kb = NULL;
//heading can be a string or a localization id
std::string headingStr;
if (heading.isString())
@@ -83,12 +82,25 @@ bool CGUIKeyboardFactory::ShowAndGetInput(std::string& aTextString, CVariant hea
else if (heading.isInteger() && heading.asInteger())
headingStr = g_localizeStrings.Get((uint32_t)heading.asInteger());
+ bool useKodiKeyboard = true;
#if defined(TARGET_DARWIN_EMBEDDED)
- kb = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKeyboardTouch>(WINDOW_DIALOG_KEYBOARD_TOUCH);
+#if defined(TARGET_DARWIN_TVOS)
+ useKodiKeyboard = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_APPLEUSEKODIKEYBOARD);
#else
- kb = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKeyboardGeneric>(WINDOW_DIALOG_KEYBOARD);
+ useKodiKeyboard = false;
+#endif // defined(TARGET_DARWIN_TVOS)
#endif
+ auto& winManager = CServiceBroker::GetGUI()->GetWindowManager();
+ CGUIKeyboard* kb = nullptr;
+ if (useKodiKeyboard)
+ kb = winManager.GetWindow<CGUIDialogKeyboardGeneric>(WINDOW_DIALOG_KEYBOARD);
+#if defined(TARGET_DARWIN_EMBEDDED)
+ else
+ kb = winManager.GetWindow<CGUIDialogKeyboardTouch>(WINDOW_DIALOG_KEYBOARD_TOUCH);
+#endif // defined(TARGET_DARWIN_EMBEDDED)
+
if (kb)
{
g_activeKeyboard = kb;
diff --git a/xbmc/guilib/TextureManager.cpp b/xbmc/guilib/TextureManager.cpp
index e99841184a..66bb3d62ca 100644
--- a/xbmc/guilib/TextureManager.cpp
+++ b/xbmc/guilib/TextureManager.cpp
@@ -26,8 +26,13 @@
#include "utils/TimeUtils.h"
#endif
#if defined(TARGET_DARWIN_IOS)
+#define WIN_SYSTEM_CLASS CWinSystemIOS
#include "ServiceBroker.h"
#include "windowing/ios/WinSystemIOS.h" // for g_Windowing in CGUITextureManager::FreeUnusedTextures
+#elif defined(TARGET_DARWIN_TVOS)
+#define WIN_SYSTEM_CLASS CWinSystemTVOS
+#include "ServiceBroker.h"
+#include "windowing/tvos/WinSystemTVOS.h" // for g_Windowing in CGUITextureManager::FreeUnusedTextures
#endif
#include "FFmpegImage.h"
@@ -501,11 +506,11 @@ void CGUITextureManager::FreeUnusedTextures(unsigned int timeDelay)
#if defined(HAS_GL) || defined(HAS_GLES)
for (unsigned int i = 0; i < m_unusedHwTextures.size(); ++i)
{
- // on ios the hw textures might be deleted from the os
- // when XBMC is backgrounded (e.x. for backgrounded music playback)
- // sanity check before delete in that case.
-#if defined(TARGET_DARWIN_IOS)
- CWinSystemIOS* winSystem = dynamic_cast<CWinSystemIOS*>(CServiceBroker::GetWinSystem());
+ // on ios/tvos the hw textures might be deleted from the os
+ // when XBMC is backgrounded (e.x. for backgrounded music playback)
+ // sanity check before delete in that case.
+#if defined(TARGET_DARWIN_EMBEDDED)
+ auto winSystem = dynamic_cast<WIN_SYSTEM_CLASS*>(CServiceBroker::GetWinSystem());
if (!winSystem->IsBackgrounded() || glIsTexture(m_unusedHwTextures[i]))
#endif
glDeleteTextures(1, (GLuint*) &m_unusedHwTextures[i]);
diff --git a/xbmc/guilib/guiinfo/GUIInfoLabels.h b/xbmc/guilib/guiinfo/GUIInfoLabels.h
index c484bfdcfe..c4ee80845c 100644
--- a/xbmc/guilib/guiinfo/GUIInfoLabels.h
+++ b/xbmc/guilib/guiinfo/GUIInfoLabels.h
@@ -438,6 +438,8 @@
#define SYSTEM_CAN_REBOOT 753
#define SYSTEM_MEDIA_AUDIO_CD 754
+#define SYSTEM_PLATFORM_DARWIN_TVOS 755
+
#define SLIDESHOW_ISPAUSED 800
#define SLIDESHOW_ISRANDOM 801
#define SLIDESHOW_ISACTIVE 802
diff --git a/xbmc/guilib/guiinfo/SystemGUIInfo.cpp b/xbmc/guilib/guiinfo/SystemGUIInfo.cpp
index 2736d2bec7..47570b99d9 100644
--- a/xbmc/guilib/guiinfo/SystemGUIInfo.cpp
+++ b/xbmc/guilib/guiinfo/SystemGUIInfo.cpp
@@ -472,6 +472,13 @@ bool CSystemGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int context
value = false;
#endif
return true;
+ case SYSTEM_PLATFORM_DARWIN_TVOS:
+#ifdef TARGET_DARWIN_TVOS
+ value = true;
+#else
+ value = false;
+#endif
+ return true;
case SYSTEM_PLATFORM_ANDROID:
#if defined(TARGET_ANDROID)
value = true;
diff --git a/xbmc/platform/darwin/DarwinUtils.mm b/xbmc/platform/darwin/DarwinUtils.mm
index b86d6f18d6..e572054b17 100644
--- a/xbmc/platform/darwin/DarwinUtils.mm
+++ b/xbmc/platform/darwin/DarwinUtils.mm
@@ -148,10 +148,17 @@ const char* CDarwinUtils::GetAppRootFolder(void)
{
if (IsIosSandboxed())
{
+#ifdef TARGET_DARWIN_TVOS
+ // writing to Documents is prohibited, more info:
+ // https://developer.apple.com/library/archive/documentation/General/Conceptual/AppleTV_PG/index.html#//apple_ref/doc/uid/TP40015241-CH12-SW5
+ // https://forums.developer.apple.com/thread/89008
+ rootFolder = "Library/Caches";
+#else
// when we are sandbox make documents our root
// so that user can access everything he needs
// via itunes sharing
rootFolder = "Documents";
+#endif
}
else
{
diff --git a/xbmc/platform/darwin/ios-common/AnnounceReceiver.mm b/xbmc/platform/darwin/ios-common/AnnounceReceiver.mm
index fc3b14b6d8..9dd7df2c5c 100644
--- a/xbmc/platform/darwin/ios-common/AnnounceReceiver.mm
+++ b/xbmc/platform/darwin/ios-common/AnnounceReceiver.mm
@@ -19,7 +19,12 @@
#include "playlists/PlayList.h"
#include "utils/Variant.h"
+#import "platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.h"
+#if defined(TARGET_DARWIN_IOS)
#import "platform/darwin/ios/XBMCController.h"
+#elif defined(TARGET_DARWIN_TVOS)
+#import "platform/darwin/tvos/XBMCController.h"
+#endif
#import <UIKit/UIKit.h>
diff --git a/xbmc/platform/darwin/ios-common/CMakeLists.txt b/xbmc/platform/darwin/ios-common/CMakeLists.txt
index 22ff753f0f..49ce23c93f 100644
--- a/xbmc/platform/darwin/ios-common/CMakeLists.txt
+++ b/xbmc/platform/darwin/ios-common/CMakeLists.txt
@@ -3,13 +3,15 @@ set(SOURCES AnnounceReceiver.mm
DarwinEmbedKeyboard.mm
DarwinEmbedKeyboardView.mm
DarwinEmbedNowPlayingInfoManager.mm
- DarwinNSUserDefaults.mm)
+ DarwinNSUserDefaults.mm
+ NSData+GZIP.m)
set(HEADERS AnnounceReceiver.h
CPUInfoDarwinEmbed.h
DarwinEmbedKeyboard.h
DarwinEmbedKeyboardView.h
DarwinEmbedNowPlayingInfoManager.h
- DarwinNSUserDefaults.h)
+ DarwinNSUserDefaults.h
+ NSData+GZIP.h)
core_add_library(platform_ios-common)
diff --git a/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboard.mm b/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboard.mm
index e0d61050ce..443c4d4e01 100644
--- a/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboard.mm
+++ b/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboard.mm
@@ -9,12 +9,19 @@
#include "DarwinEmbedKeyboard.h"
#include "platform/darwin/ios-common/DarwinEmbedKeyboardView.h"
+#if defined(TARGET_DARWIN_IOS)
#include "platform/darwin/ios/IOSKeyboardView.h"
#include "platform/darwin/ios/XBMCController.h"
+#define KEYBOARDVIEW_CLASS IOSKeyboardView
+#elif defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/TVOSKeyboardView.h"
+#include "platform/darwin/tvos/XBMCController.h"
+#define KEYBOARDVIEW_CLASS TVOSKeyboardView
+#endif
struct CDarwinEmbedKeyboardImpl
{
- IOSKeyboardView* g_pKeyboard = nil;
+ KEYBOARDVIEW_CLASS* g_pKeyboard = nil;
};
CDarwinEmbedKeyboard::CDarwinEmbedKeyboard()
@@ -37,13 +44,12 @@ bool CDarwinEmbedKeyboard::ShowAndGetInput(char_callback_t pCallback,
if (m_impl->g_pKeyboard)
return false;
- //! @Todo generalise this block for platform
//create the keyboardview
- IOSKeyboardView* __block keyboardView;
+ KEYBOARDVIEW_CLASS* __block keyboardView;
dispatch_sync(dispatch_get_main_queue(), ^{
// assume we are only drawn on the mainscreen ever!
auto keyboardFrame = [g_xbmcController fullscreenSubviewFrame];
- keyboardView = [[IOSKeyboardView alloc] initWithFrame:keyboardFrame];
+ keyboardView = [[KEYBOARDVIEW_CLASS alloc] initWithFrame:keyboardFrame];
});
////////////////////////////////////////////
if (!keyboardView)
@@ -66,6 +72,9 @@ bool CDarwinEmbedKeyboard::ShowAndGetInput(char_callback_t pCallback,
if (!m_canceled)
{
[m_impl->g_pKeyboard setCancelFlag:&m_canceled];
+ // workaround for multiple keyboardviews running quickly - race condition
+ sleep(1);
+
[m_impl->g_pKeyboard activate]; // blocks and shows keyboard
// user is done - get resulted text and confirmation
confirmed = [m_impl->g_pKeyboard isConfirmed];
diff --git a/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.h b/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.h
index a50b616d8d..a153c147f4 100644
--- a/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.h
+++ b/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.h
@@ -20,6 +20,7 @@
@property(getter=isConfirmed) BOOL confirmed;
@property(assign) CDarwinEmbedKeyboard* darwinEmbedKeyboard;
@property(nonatomic, readonly) NSString* text;
+@property(nonatomic, assign, getter=isKeyboardVisible) bool keyboardVisible;
- (void)setHeading:(NSString*)heading;
- (void)setHidden:(BOOL)hidden;
diff --git a/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.mm b/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.mm
index c0365c614a..bbd7fe56d5 100644
--- a/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.mm
+++ b/xbmc/platform/darwin/ios-common/DarwinEmbedKeyboardView.mm
@@ -13,7 +13,11 @@
#include "threads/Event.h"
#include "utils/log.h"
-#import "platform/darwin/ios/XBMCController.h"
+#if defined(TARGET_DARWIN_IOS)
+#include "platform/darwin/ios/XBMCController.h"
+#elif defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/XBMCController.h"
+#endif
static CEvent keyboardFinishedEvent;
@@ -21,6 +25,7 @@ static CEvent keyboardFinishedEvent;
@synthesize confirmed = m_confirmed;
@synthesize darwinEmbedKeyboard = m_darwinEmbedKeyboard;
+@synthesize keyboardVisible = m_keyboardVisible;
- (instancetype)initWithFrame:(CGRect)frame
{
@@ -30,6 +35,7 @@ static CEvent keyboardFinishedEvent;
m_canceled = nullptr;
m_deactivated = NO;
+ m_keyboardVisible = false;
auto textField = [UITextField new];
textField.translatesAutoresizingMaskIntoConstraints = NO;
@@ -59,6 +65,13 @@ static CEvent keyboardFinishedEvent;
- (BOOL)textFieldShouldReturn:(UITextField*)textField
{
m_confirmed = YES;
+ [textField resignFirstResponder];
+ return YES;
+}
+
+- (BOOL)textFieldShouldEndEditing:(UITextField*)textField
+{
+ CLog::Log(LOGDEBUG, "{}: keyboard IsShowing {}", __PRETTY_FUNCTION__, self.isKeyboardVisible);
return YES;
}
@@ -83,16 +96,12 @@ static CEvent keyboardFinishedEvent;
// we are waiting on the user finishing the keyboard
while (!keyboardFinishedEvent.WaitMSec(500))
{
- if (nullptr != m_canceled && *m_canceled)
- {
- [self deactivate];
- m_canceled = nullptr;
- }
}
}
- (void)deactivate
{
+ CLog::Log(LOGDEBUG, "{}: keyboard IsShowing {}", __PRETTY_FUNCTION__, self.isKeyboardVisible);
m_deactivated = YES;
// invalidate our callback object
@@ -125,12 +134,6 @@ static CEvent keyboardFinishedEvent;
m_inputTextField.text = aText;
[self textChanged:nil];
});
-
- if (closeKeyboard)
- {
- m_confirmed = YES;
- [self deactivate];
- }
}
- (void)setHeading:(NSString*)heading
diff --git a/xbmc/platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.mm b/xbmc/platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.mm
index 24f88f508e..5b5c9a9f26 100644
--- a/xbmc/platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.mm
+++ b/xbmc/platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.mm
@@ -8,7 +8,11 @@
#import "DarwinEmbedNowPlayingInfoManager.h"
-#import "platform/darwin/ios/XBMCController.h"
+#if defined(TARGET_DARWIN_IOS)
+#include "platform/darwin/ios/XBMCController.h"
+#elif defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/XBMCController.h"
+#endif
#import <MediaPlayer/MediaPlayer.h>
@@ -80,7 +84,9 @@
self.playbackState = DARWINEMBED_PLAYBACK_PLAYING;
+#if defined(TARGET_DARWIN_IOS)
[g_xbmcController disableNetworkAutoSuspend];
+#endif
}
- (void)OnSpeedChanged:(NSDictionary*)item
@@ -98,8 +104,10 @@
{
self.playbackState = DARWINEMBED_PLAYBACK_PAUSED;
+#if defined(TARGET_DARWIN_IOS)
// schedule set network auto suspend state for save power if idle.
[g_xbmcController rescheduleNetworkAutoSuspend];
+#endif
}
- (void)onStop:(NSDictionary*)item
@@ -108,8 +116,10 @@
self.playbackState = DARWINEMBED_PLAYBACK_STOPPED;
+#if defined(TARGET_DARWIN_IOS)
// delay set network auto suspend state in case we are switching playing item.
[g_xbmcController rescheduleNetworkAutoSuspend];
+#endif
}
- (void)observeValueForKeyPath:(NSString*)keyPath
diff --git a/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.h b/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.h
index 480dec587b..121b669a96 100644
--- a/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.h
+++ b/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.h
@@ -1,3 +1,4 @@
+#pragma once
/*
* Copyright (C) 2015 Team MrMC
* https://github.com/MrMC
@@ -6,8 +7,6 @@
* See LICENSES/README.md for more information.
*/
-#pragma once
-
#include <string>
class CDarwinNSUserDefaults
@@ -15,14 +14,25 @@ class CDarwinNSUserDefaults
public:
static bool Synchronize();
- static bool GetKey(const std::string &key, std::string &value);
- static bool SetKey(const std::string &key, const std::string &value, bool synchronize);
- static bool DeleteKey(const std::string &key, bool synchronize);
- static bool KeyExists(const std::string &key);
+ static void GetDirectoryContents(const std::string& path, std::vector<std::string>& contents);
+ static bool GetKey(const std::string& key, std::string& value);
+ static bool GetKeyData(const std::string& key, void* lpBuf, size_t& uiBufSize);
+ static bool SetKey(const std::string& key, const std::string& value, bool synchronize);
+ static bool SetKeyData(const std::string& key,
+ const void* lpBuf,
+ size_t uiBufSize,
+ bool synchronize);
+ static bool DeleteKey(const std::string& key, bool synchronize);
+ static bool KeyExists(const std::string& key);
- static bool IsKeyFromPath(const std::string &key);
- static bool GetKeyFromPath(const std::string &path, std::string &value);
- static bool SetKeyFromPath(const std::string &path, const std::string &value, bool synchronize);
- static bool DeleteKeyFromPath(const std::string &path, bool synchronize);
- static bool KeyFromPathExists(const std::string &key);
+ static bool IsKeyFromPath(const std::string& key);
+ static bool GetKeyFromPath(const std::string& path, std::string& value);
+ static bool GetKeyDataFromPath(const std::string& path, void* lpBuf, size_t& uiBufSize);
+ static bool SetKeyFromPath(const std::string& path, const std::string& value, bool synchronize);
+ static bool SetKeyDataFromPath(const std::string& path,
+ const void* lpBuf,
+ size_t uiBufSize,
+ bool synchronize);
+ static bool DeleteKeyFromPath(const std::string& path, bool synchronize);
+ static bool KeyFromPathExists(const std::string& path);
};
diff --git a/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.mm b/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.mm
index de8c3f7b5c..e2b655f7f0 100644
--- a/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.mm
+++ b/xbmc/platform/darwin/ios-common/DarwinNSUserDefaults.mm
@@ -6,23 +6,54 @@
* See LICENSES/README.md for more information.
*/
-#import "platform/darwin/DarwinNSUserDefaults.h"
+#import "DarwinNSUserDefaults.h"
#import "filesystem/SpecialProtocol.h"
+#import "utils/URIUtils.h"
#import "utils/log.h"
+#import "platform/darwin/ios-common/NSData+GZIP.h"
+#import "platform/darwin/tvos/filesystem/TVOSFileUtils.h"
+
+#import <string>
+
+#import <Foundation/NSData.h>
+#import <Foundation/NSDictionary.h>
#import <Foundation/NSString.h>
#import <Foundation/NSUserDefaults.h>
+static bool firstLookup = true;
-static bool translatePathIntoKey(const std::string &path, std::string &key)
+static bool translatePathIntoKey(const std::string& path, std::string& key)
{
+ if (firstLookup)
+ {
+ NSDictionary<NSString*, id>* dict =
+ [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+ for (NSString* aKey in dict.allKeys)
+ {
+ // do something like a log:
+ if (![aKey hasPrefix:@"/userdata/"])
+ continue;
+
+ NSData* nsdata = [defaults dataForKey:aKey];
+ size_t size = nsdata.length;
+ CLog::Log(LOGNOTICE, "nsuserdefaults: %s with size %ld", aKey.UTF8String, size);
+ }
+ firstLookup = false;
+ }
size_t pos;
std::string translated_key = CSpecialProtocol::TranslatePath(path);
- if ((pos = translated_key.find("Caches/userdata")) != std::string::npos)
+ std::string userDataDir =
+ URIUtils::AddFileToFolder(CTVOSFileUtils::GetUserHomeDirectory(), "userdata");
+ if (translated_key.find(userDataDir) != std::string::npos)
{
- key = translated_key.erase(0, pos);
- return true;
+ if ((pos = translated_key.find("/userdata")) != std::string::npos)
+ {
+ key = translated_key.erase(0, pos);
+ return true;
+ }
}
return false;
@@ -33,16 +64,53 @@ bool CDarwinNSUserDefaults::Synchronize()
return [[NSUserDefaults standardUserDefaults] synchronize] == YES;
}
-bool CDarwinNSUserDefaults::GetKey(const std::string &key, std::string &value)
+void CDarwinNSUserDefaults::GetDirectoryContents(const std::string& path,
+ std::vector<std::string>& contents)
+{
+ // tvos path adds /private/../..
+ // We need to strip this as GetUserHomeDirectory() doesnt have private in the path
+ std::string subpath = path;
+ const std::string& str_private = "/private";
+ size_t pos = subpath.find(str_private.c_str(), 0, str_private.length());
+
+ if (pos != std::string::npos)
+ subpath.erase(pos, str_private.length());
+
+ std::string userDataDir =
+ URIUtils::AddFileToFolder(CTVOSFileUtils::GetUserHomeDirectory(), "userdata");
+
+ if (subpath.find(userDataDir) == std::string::npos)
+ return;
+
+ NSDictionary<NSString*, id>* dict =
+ [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
+ for (NSString* aKey in dict.allKeys)
+ {
+ // do something like a log:
+ if (![aKey hasPrefix:@"/userdata/"])
+ continue;
+
+ std::string keypath = aKey.UTF8String;
+ std::string fullKeyPath =
+ URIUtils::AddFileToFolder(CTVOSFileUtils::GetUserHomeDirectory(), keypath);
+ std::string endingDirectory = URIUtils::GetDirectory(fullKeyPath);
+ if (subpath == endingDirectory)
+ {
+ contents.push_back(fullKeyPath);
+ }
+ }
+}
+
+bool CDarwinNSUserDefaults::GetKey(const std::string& key, std::string& value)
{
if (!key.empty())
{
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
- NSString *nsstring_key = [NSString stringWithUTF8String: key.c_str()];
- NSString *nsstring_value = [defaults stringForKey:nsstring_key];
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+ NSString* nsstring_key = @(key.c_str());
+ NSString* nsstring_value = [defaults stringForKey:nsstring_key];
if (nsstring_value)
{
- value = [nsstring_value UTF8String];
+ value = nsstring_value.UTF8String;
if (!value.empty())
return true;
}
@@ -51,14 +119,47 @@ bool CDarwinNSUserDefaults::GetKey(const std::string &key, std::string &value)
return false;
}
-bool CDarwinNSUserDefaults::SetKey(const std::string &key, const std::string &value, bool synchronize)
+bool CDarwinNSUserDefaults::GetKeyData(const std::string& key, void* lpBuf, size_t& uiBufSize)
+{
+ if (key.empty())
+ {
+ uiBufSize = 0;
+ return false;
+ }
+
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+ NSString* nsstring_key = @(key.c_str());
+ NSData* nsdata = [defaults dataForKey:nsstring_key];
+ if (!nsdata)
+ {
+ uiBufSize = 0;
+ return false;
+ }
+
+ NSData* decompressed = nsdata;
+ if ([nsdata isGzippedData])
+ decompressed = [nsdata gunzippedData];
+
+ uiBufSize = decompressed.length;
+
+ // call was to get size of file
+ if (lpBuf == nullptr)
+ return true;
+
+ memcpy(lpBuf, decompressed.bytes, decompressed.length);
+ return true;
+}
+
+bool CDarwinNSUserDefaults::SetKey(const std::string& key,
+ const std::string& value,
+ bool synchronize)
{
if (!key.empty() && !value.empty())
{
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
- NSString *nsstring_key = [NSString stringWithUTF8String: key.c_str()];
- NSString *nsstring_value = [NSString stringWithUTF8String: value.c_str()];
+ NSString* nsstring_key = @(key.c_str());
+ NSString* nsstring_value = @(value.c_str());
[defaults setObject:nsstring_value forKey:nsstring_key];
if (synchronize)
@@ -70,12 +171,37 @@ bool CDarwinNSUserDefaults::SetKey(const std::string &key, const std::string &va
return false;
}
-bool CDarwinNSUserDefaults::DeleteKey(const std::string &key, bool synchronize)
+bool CDarwinNSUserDefaults::SetKeyData(const std::string& key,
+ const void* lpBuf,
+ size_t uiBufSize,
+ bool synchronize)
+{
+ if (!key.empty() && lpBuf != nullptr && uiBufSize > 0)
+ {
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+ NSString* nsstring_key = @(key.c_str());
+ NSData* nsdata_value = [NSData dataWithBytes:lpBuf length:uiBufSize];
+
+ NSData* compressed = [nsdata_value gzippedData];
+ CLog::Log(LOGDEBUG, "NSUSerDefaults: compressed %s from %ld to %ld", key.c_str(), uiBufSize,
+ compressed.length);
+
+ [defaults setObject:compressed forKey:nsstring_key];
+ if (synchronize)
+ return [defaults synchronize] == YES;
+ else
+ return true;
+ }
+
+ return false;
+}
+
+bool CDarwinNSUserDefaults::DeleteKey(const std::string& key, bool synchronize)
{
if (!key.empty())
{
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
- NSString *nsstring_key = [NSString stringWithUTF8String: key.c_str()];
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+ NSString* nsstring_key = @(key.c_str());
[defaults removeObjectForKey:nsstring_key];
if (synchronize)
return [defaults synchronize] == YES;
@@ -86,11 +212,11 @@ bool CDarwinNSUserDefaults::DeleteKey(const std::string &key, bool synchronize)
return false;
}
-bool CDarwinNSUserDefaults::KeyExists(const std::string &key)
+bool CDarwinNSUserDefaults::KeyExists(const std::string& key)
{
if (!key.empty())
{
- NSString *nsstring_key = [NSString stringWithUTF8String: key.c_str()];
+ NSString* nsstring_key = @(key.c_str());
if ([[NSUserDefaults standardUserDefaults] objectForKey:nsstring_key])
return true;
}
@@ -98,7 +224,7 @@ bool CDarwinNSUserDefaults::KeyExists(const std::string &key)
return false;
}
-bool CDarwinNSUserDefaults::IsKeyFromPath(const std::string &path)
+bool CDarwinNSUserDefaults::IsKeyFromPath(const std::string& path)
{
std::string translated_key;
if (translatePathIntoKey(path, translated_key) && !translated_key.empty())
@@ -110,7 +236,7 @@ bool CDarwinNSUserDefaults::IsKeyFromPath(const std::string &path)
return false;
}
-bool CDarwinNSUserDefaults::GetKeyFromPath(const std::string &path, std::string &value)
+bool CDarwinNSUserDefaults::GetKeyFromPath(const std::string& path, std::string& value)
{
std::string translated_key;
if (translatePathIntoKey(path, translated_key) && !translated_key.empty())
@@ -119,7 +245,20 @@ bool CDarwinNSUserDefaults::GetKeyFromPath(const std::string &path, std::string
return false;
}
-bool CDarwinNSUserDefaults::SetKeyFromPath(const std::string &path, const std::string &value, bool synchronize)
+bool CDarwinNSUserDefaults::GetKeyDataFromPath(const std::string& path,
+ void* lpBuf,
+ size_t& uiBufSize)
+{
+ std::string translated_key;
+ if (translatePathIntoKey(path, translated_key) && !translated_key.empty())
+ return CDarwinNSUserDefaults::GetKeyData(translated_key, lpBuf, uiBufSize);
+
+ return false;
+}
+
+bool CDarwinNSUserDefaults::SetKeyFromPath(const std::string& path,
+ const std::string& value,
+ bool synchronize)
{
std::string translated_key;
if (translatePathIntoKey(path, translated_key) && !translated_key.empty() && !value.empty())
@@ -128,7 +267,19 @@ bool CDarwinNSUserDefaults::SetKeyFromPath(const std::string &path, const std::s
return false;
}
-bool CDarwinNSUserDefaults::DeleteKeyFromPath(const std::string &path, bool synchronize)
+bool CDarwinNSUserDefaults::SetKeyDataFromPath(const std::string& path,
+ const void* lpBuf,
+ size_t uiBufSize,
+ bool synchronize)
+{
+ std::string translated_key;
+ if (translatePathIntoKey(path, translated_key) && !translated_key.empty())
+ return CDarwinNSUserDefaults::SetKeyData(translated_key, lpBuf, uiBufSize, synchronize);
+
+ return false;
+}
+
+bool CDarwinNSUserDefaults::DeleteKeyFromPath(const std::string& path, bool synchronize)
{
std::string translated_key;
if (translatePathIntoKey(path, translated_key) && !translated_key.empty())
@@ -137,7 +288,7 @@ bool CDarwinNSUserDefaults::DeleteKeyFromPath(const std::string &path, bool sync
return false;
}
-bool CDarwinNSUserDefaults::KeyFromPathExists(const std::string &path)
+bool CDarwinNSUserDefaults::KeyFromPathExists(const std::string& path)
{
std::string translated_key;
if (translatePathIntoKey(path, translated_key) && !translated_key.empty())
diff --git a/xbmc/platform/darwin/ios-common/NSData+GZIP.h b/xbmc/platform/darwin/ios-common/NSData+GZIP.h
new file mode 100644
index 0000000000..09628ad6ed
--- /dev/null
+++ b/xbmc/platform/darwin/ios-common/NSData+GZIP.h
@@ -0,0 +1,43 @@
+//
+// GZIP.h
+//
+// Version 1.2.3
+//
+// Created by Nick Lockwood on 03/06/2012.
+// Copyright (C) 2012 Charcoal Design
+//
+// Distributed under the permissive MIT license
+// Get the latest version from here:
+//
+// https://github.com/nicklockwood/GZIP
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+#import <Foundation/Foundation.h>
+
+
+@interface NSData (GZIP)
+
+- (nullable NSData *)gzippedDataWithCompressionLevel:(float)level;
+- (nullable NSData *)gzippedData;
+- (nullable NSData *)gunzippedData;
+- (BOOL)isGzippedData;
+
+@end
diff --git a/xbmc/platform/darwin/ios-common/NSData+GZIP.m b/xbmc/platform/darwin/ios-common/NSData+GZIP.m
new file mode 100644
index 0000000000..82c4c9554b
--- /dev/null
+++ b/xbmc/platform/darwin/ios-common/NSData+GZIP.m
@@ -0,0 +1,135 @@
+//
+// GZIP.m
+//
+// Version 1.2.3
+//
+// Created by Nick Lockwood on 03/06/2012.
+// Copyright (C) 2012 Charcoal Design
+//
+// Distributed under the permissive MIT license
+// Get the latest version from here:
+//
+// https://github.com/nicklockwood/GZIP
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+
+#import "NSData+GZIP.h"
+#import <zlib.h>
+
+
+#pragma clang diagnostic ignored "-Wcast-qual"
+
+
+@implementation NSData (GZIP)
+
+- (NSData *)gzippedDataWithCompressionLevel:(float)level
+{
+ if (self.length == 0 || [self isGzippedData])
+ {
+ return self;
+ }
+
+ z_stream stream;
+ stream.zalloc = Z_NULL;
+ stream.zfree = Z_NULL;
+ stream.opaque = Z_NULL;
+ stream.avail_in = (uint)self.length;
+ stream.next_in = (Bytef *)(void *)self.bytes;
+ stream.total_out = 0;
+ stream.avail_out = 0;
+
+ static const NSUInteger ChunkSize = 16384;
+
+ NSMutableData *output = nil;
+ int compression = (level < 0.0f)? Z_DEFAULT_COMPRESSION: (int)(roundf(level * 9));
+ if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK)
+ {
+ output = [NSMutableData dataWithLength:ChunkSize];
+ while (stream.avail_out == 0)
+ {
+ if (stream.total_out >= output.length)
+ {
+ output.length += ChunkSize;
+ }
+ stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
+ stream.avail_out = (uInt)(output.length - stream.total_out);
+ deflate(&stream, Z_FINISH);
+ }
+ deflateEnd(&stream);
+ output.length = stream.total_out;
+ }
+
+ return output;
+}
+
+- (NSData *)gzippedData
+{
+ return [self gzippedDataWithCompressionLevel:-1.0f];
+}
+
+- (NSData *)gunzippedData
+{
+ if (self.length == 0 || ![self isGzippedData])
+ {
+ return self;
+ }
+
+ z_stream stream;
+ stream.zalloc = Z_NULL;
+ stream.zfree = Z_NULL;
+ stream.avail_in = (uint)self.length;
+ stream.next_in = (Bytef *)self.bytes;
+ stream.total_out = 0;
+ stream.avail_out = 0;
+
+ NSMutableData *output = nil;
+ if (inflateInit2(&stream, 47) == Z_OK)
+ {
+ int status = Z_OK;
+ output = [NSMutableData dataWithCapacity:self.length * 2];
+ while (status == Z_OK)
+ {
+ if (stream.total_out >= output.length)
+ {
+ output.length += self.length / 2;
+ }
+ stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
+ stream.avail_out = (uInt)(output.length - stream.total_out);
+ status = inflate (&stream, Z_SYNC_FLUSH);
+ }
+ if (inflateEnd(&stream) == Z_OK)
+ {
+ if (status == Z_STREAM_END)
+ {
+ output.length = stream.total_out;
+ }
+ }
+ }
+
+ return output;
+}
+
+- (BOOL)isGzippedData
+{
+ const UInt8 *bytes = (const UInt8 *)self.bytes;
+ return (self.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
+}
+
+@end
diff --git a/xbmc/platform/darwin/ios/IOSKeyboardView.mm b/xbmc/platform/darwin/ios/IOSKeyboardView.mm
index 74ec08a705..cdbad6ebc5 100644
--- a/xbmc/platform/darwin/ios/IOSKeyboardView.mm
+++ b/xbmc/platform/darwin/ios/IOSKeyboardView.mm
@@ -15,14 +15,12 @@ static const CGFloat INPUT_BOX_HEIGHT = 30;
@interface IOSKeyboardView ()
@property(nonatomic, weak) UIView* textFieldContainer;
@property(nonatomic, weak) NSLayoutConstraint* containerBottomConstraint;
-@property(nonatomic, assign, getter=isKeyboardVisible) bool keyboardVisible;
@end
@implementation IOSKeyboardView
@synthesize textFieldContainer = m_textFieldContainer;
@synthesize containerBottomConstraint = m_containerBottomConstraint;
-@synthesize keyboardVisible = m_keyboardVisible;
- (instancetype)initWithFrame:(CGRect)frame
{
@@ -30,8 +28,6 @@ static const CGFloat INPUT_BOX_HEIGHT = 30;
if (!self)
return nil;
- m_keyboardVisible = false;
-
self.backgroundColor = [UIColor colorWithWhite:0 alpha:0.1];
auto notificationCenter = NSNotificationCenter.defaultCenter;
@@ -100,20 +96,6 @@ static const CGFloat INPUT_BOX_HEIGHT = 30;
[self deactivate];
}
-- (BOOL)textFieldShouldEndEditing:(UITextField*)textField
-{
- CLog::Log(LOGDEBUG, "{}: keyboard IsShowing {}", __PRETTY_FUNCTION__, self.isKeyboardVisible);
- return YES;
-}
-
-- (BOOL)textFieldShouldReturn:(UITextField*)textField
-{
- auto result = [super textFieldShouldReturn:textField];
- if (result)
- [textField resignFirstResponder];
- return result;
-}
-
- (void)keyboardDidChangeFrame:(NSNotification*)notification
{
auto keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
@@ -137,10 +119,16 @@ static const CGFloat INPUT_BOX_HEIGHT = 30;
[self deactivate];
}
-- (void)deactivate
+- (void)setKeyboardText:(NSString*)aText closeKeyboard:(BOOL)closeKeyboard
{
- CLog::Log(LOGDEBUG, "{}: keyboard IsShowing {}", __PRETTY_FUNCTION__, self.isKeyboardVisible);
- [super deactivate];
+
+ [super setKeyboardText:aText closeKeyboard:closeKeyboard];
+
+ if (closeKeyboard)
+ {
+ self.confirmed = YES;
+ [self deactivate];
+ }
}
@end
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/Contents.json
new file mode 100644
index 0000000000..ea3ed3a78f
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/Contents.json
@@ -0,0 +1,32 @@
+{
+ "assets" : [
+ {
+ "size" : "1280x768",
+ "idiom" : "tv",
+ "filename" : "icon_appstore.imagestack",
+ "role" : "primary-app-icon"
+ },
+ {
+ "size" : "400x240",
+ "idiom" : "tv",
+ "filename" : "icon.imagestack",
+ "role" : "primary-app-icon"
+ },
+ {
+ "size" : "2320x720",
+ "idiom" : "tv",
+ "filename" : "topshelf_wide.imageset",
+ "role" : "top-shelf-image-wide"
+ },
+ {
+ "size" : "1920x720",
+ "idiom" : "tv",
+ "filename" : "topshelf.imageset",
+ "role" : "top-shelf-image"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Contents.json
new file mode 100644
index 0000000000..45c0e8710f
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Contents.json
@@ -0,0 +1,26 @@
+{
+ "layers" : [
+ {
+ "filename" : "Layer1.imagestacklayer"
+ },
+ {
+ "filename" : "Layer2.imagestacklayer"
+ },
+ {
+ "filename" : "Layer3.imagestacklayer"
+ },
+ {
+ "filename" : "Layer4.imagestacklayer"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "canvasSize" : {
+ "width" : 400,
+ "height" : 240
+ }
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/.gitignore b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/.gitignore
new file mode 100644
index 0000000000..b7b70d933c
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/.gitignore
@@ -0,0 +1 @@
+image.png
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..d51a8f4854
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,18 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "filename" : "image.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "filename" : "image@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/image@2x.png b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/image@2x.png
new file mode 100644
index 0000000000..08204cb690
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Content.imageset/image@2x.png
Binary files differ
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..af0b7e34b1
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer1.imagestacklayer/Contents.json
@@ -0,0 +1,16 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "frame-size" : {
+ "width" : 400,
+ "height" : 240
+ },
+ "frame-center" : {
+ "x" : 200,
+ "y" : 120
+ }
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/.gitignore b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/.gitignore
new file mode 100644
index 0000000000..b7b70d933c
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/.gitignore
@@ -0,0 +1 @@
+image.png
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..d51a8f4854
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,18 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "filename" : "image.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "filename" : "image@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/image@2x.png b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/image@2x.png
new file mode 100644
index 0000000000..b07a17d2e4
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Content.imageset/image@2x.png
Binary files differ
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..af0b7e34b1
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer2.imagestacklayer/Contents.json
@@ -0,0 +1,16 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "frame-size" : {
+ "width" : 400,
+ "height" : 240
+ },
+ "frame-center" : {
+ "x" : 200,
+ "y" : 120
+ }
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/.gitignore b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/.gitignore
new file mode 100644
index 0000000000..b7b70d933c
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/.gitignore
@@ -0,0 +1 @@
+image.png
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..d51a8f4854
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,18 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "filename" : "image.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "filename" : "image@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/image@2x.png b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/image@2x.png
new file mode 100644
index 0000000000..7cd92eecd6
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Content.imageset/image@2x.png
Binary files differ
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..af0b7e34b1
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer3.imagestacklayer/Contents.json
@@ -0,0 +1,16 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "frame-size" : {
+ "width" : 400,
+ "height" : 240
+ },
+ "frame-center" : {
+ "x" : 200,
+ "y" : 120
+ }
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/.gitignore b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/.gitignore
new file mode 100644
index 0000000000..b7b70d933c
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/.gitignore
@@ -0,0 +1 @@
+image.png
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..d51a8f4854
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,18 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "filename" : "image.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "filename" : "image@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/image@2x.png b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/image@2x.png
new file mode 100644
index 0000000000..801cdccaee
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Content.imageset/image@2x.png
Binary files differ
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..af0b7e34b1
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon.imagestack/Layer4.imagestacklayer/Contents.json
@@ -0,0 +1,16 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "frame-size" : {
+ "width" : 400,
+ "height" : 240
+ },
+ "frame-center" : {
+ "x" : 200,
+ "y" : 120
+ }
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..bed92641a9
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..2d92bd53fd
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Back.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Contents.json
new file mode 100644
index 0000000000..8bf75d9f59
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Contents.json
@@ -0,0 +1,17 @@
+{
+ "layers" : [
+ {
+ "filename" : "Front.imagestacklayer"
+ },
+ {
+ "filename" : "Middle.imagestacklayer"
+ },
+ {
+ "filename" : "Back.imagestacklayer"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..bed92641a9
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..2d92bd53fd
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Front.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
new file mode 100644
index 0000000000..bed92641a9
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Contents.json
new file mode 100644
index 0000000000..2d92bd53fd
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/icon_appstore.imagestack/Middle.imagestacklayer/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf.imageset/Contents.json
new file mode 100644
index 0000000000..bed92641a9
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/.gitignore b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/.gitignore
new file mode 100644
index 0000000000..f3f2b7cf65
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/.gitignore
@@ -0,0 +1 @@
+image@2x.png
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/Contents.json
new file mode 100644
index 0000000000..d51a8f4854
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/Contents.json
@@ -0,0 +1,18 @@
+{
+ "images" : [
+ {
+ "idiom" : "tv",
+ "filename" : "image.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "tv",
+ "filename" : "image@2x.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/image.png b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/image.png
new file mode 100644
index 0000000000..fc71debafa
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Assets.brandassets/topshelf_wide.imageset/image.png
Binary files differ
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/Contents.json
new file mode 100644
index 0000000000..2d92bd53fd
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/.gitignore b/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/.gitignore
new file mode 100644
index 0000000000..93140602aa
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/.gitignore
@@ -0,0 +1 @@
+splash@2x.jpg
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json b/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json
new file mode 100644
index 0000000000..053b4a34bb
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/Contents.json
@@ -0,0 +1,24 @@
+{
+ "images" : [
+ {
+ "orientation" : "landscape",
+ "idiom" : "tv",
+ "filename" : "splash.jpg",
+ "extent" : "full-screen",
+ "minimum-system-version" : "9.0",
+ "scale" : "1x"
+ },
+ {
+ "orientation" : "landscape",
+ "idiom" : "tv",
+ "filename" : "splash@2x.jpg",
+ "extent" : "full-screen",
+ "minimum-system-version" : "11.0",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/splash.jpg b/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/splash.jpg
new file mode 120000
index 0000000000..38457c1d9a
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Assets.xcassets/LaunchImage.launchimage/splash.jpg
@@ -0,0 +1 @@
+../../../../../../media/splash.jpg \ No newline at end of file
diff --git a/xbmc/platform/darwin/tvos/CMakeLists.txt b/xbmc/platform/darwin/tvos/CMakeLists.txt
new file mode 100644
index 0000000000..099511dd07
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/CMakeLists.txt
@@ -0,0 +1,20 @@
+set(SOURCES PreflightHandler.mm
+ TVOSDisplayManager.mm
+ TVOSEAGLView.mm
+ TVOSKeyboardView.mm
+ TVOSSettingsHandler.mm
+ tvosShared.m
+ TVOSTopShelf.mm
+ XBMCController.mm)
+
+set(HEADERS PreflightHandler.h
+ TVOSDisplayManager.h
+ TVOSEAGLView.h
+ TVOSKeyboardView.h
+ TVOSSettingsHandler.h
+ tvosShared.h
+ TVOSTopShelf.h
+ XBMCApplication.h
+ XBMCController.h)
+
+core_add_library(platform_tvos)
diff --git a/xbmc/platform/darwin/tvos/FrameworkSeed_Info.plist b/xbmc/platform/darwin/tvos/FrameworkSeed_Info.plist
new file mode 100644
index 0000000000..925ca87bae
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/FrameworkSeed_Info.plist
@@ -0,0 +1,35 @@
+<?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>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>$(CURRENT_PROJECT_VERSION)</string>
+ <key>NSPrincipalClass</key>
+ <string></string>
+ <key>MinimumOSVersion</key>
+ <string>9.0</string>
+ <key>CFBundleSupportedPlatforms</key>
+ <array>
+ <string>AppleTVOS</string>
+ </array>
+ <key>UIDeviceFamily</key>
+ <array>
+ <integer>3</integer>
+ </array></dict>
+</plist>
diff --git a/xbmc/platform/darwin/tvos/Info.plist.in b/xbmc/platform/darwin/tvos/Info.plist.in
new file mode 100644
index 0000000000..c77c186568
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Info.plist.in
@@ -0,0 +1,51 @@
+<?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>English</string>
+ <key>CFBundleDisplayName</key>
+ <string>@APP_NAME@</string>
+ <key>CFBundleExecutable</key>
+ <string>@APP_NAME@</string>
+ <key>CFBundleIdentifier</key>
+ <string>@PLATFORM_BUNDLE_IDENTIFIER@</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>@APP_NAME@</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>@APP_VERSION@</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>@PLATFORM_BUNDLE_IDENTIFIER@</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>kodi</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleVersion</key>
+ <string>@APP_SCMID@</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>NSAppTransportSecurity</key>
+ <dict>
+ <key>NSAllowsArbitraryLoads</key>
+ <true/>
+ </dict>
+ <key>UIFileSharingEnabled</key>
+ <string>YES</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ <string>opengles-3</string>
+ </array>
+</dict>
+</plist>
diff --git a/xbmc/platform/darwin/tvos/Kodi.entitlements.in b/xbmc/platform/darwin/tvos/Kodi.entitlements.in
new file mode 100644
index 0000000000..70e501bca5
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/Kodi.entitlements.in
@@ -0,0 +1,10 @@
+<?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>com.apple.security.application-groups</key>
+ <array>
+ <string>group.@PLATFORM_BUNDLE_IDENTIFIER@</string>
+ </array>
+</dict>
+</plist>
diff --git a/xbmc/platform/darwin/tvos/PreflightHandler.h b/xbmc/platform/darwin/tvos/PreflightHandler.h
new file mode 100644
index 0000000000..54198150b0
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/PreflightHandler.h
@@ -0,0 +1,31 @@
+#pragma once
+/*
+ * Copyright (C) 2016 Team MrMC
+ * https://github.com/MrMC
+ *
+ * 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 2, 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 MrMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdint.h>
+
+class CPreflightHandler
+{
+public:
+ static uint64_t NSUserDefaultsSize();
+ static void NSUserDefaultsPurge(const char* prefix);
+ static void MigrateUserdataXMLToNSUserDefaults();
+ static void CheckForRemovedCacheFolder();
+};
diff --git a/xbmc/platform/darwin/tvos/PreflightHandler.mm b/xbmc/platform/darwin/tvos/PreflightHandler.mm
new file mode 100644
index 0000000000..7dce78842b
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/PreflightHandler.mm
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2016 Team MrMC
+ * https://github.com/MrMC
+ *
+ * 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 2, 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 MrMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#import "PreflightHandler.h"
+
+#include "URL.h"
+#include "filesystem/File.h"
+#include "utils/log.h"
+
+#import "platform/darwin/ios-common/DarwinNSUserDefaults.h"
+#import "platform/darwin/tvos/filesystem/TVOSFile.h"
+#import "platform/darwin/tvos/filesystem/TVOSFileUtils.h"
+#include "platform/posix/filesystem/PosixFile.h"
+
+#import <UIKit/UIKit.h>
+
+uint64_t CPreflightHandler::NSUserDefaultsSize()
+{
+ auto libraryDir =
+ NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES)[0];
+ auto filepath = [libraryDir
+ stringByAppendingPathComponent:[NSString
+ stringWithFormat:@"/Preferences/%@.plist",
+ [NSBundle mainBundle].bundleIdentifier]];
+ auto fileSize =
+ [[[NSFileManager defaultManager] attributesOfItemAtPath:filepath error:nil][NSFileSize]
+ unsignedLongLongValue];
+
+ return fileSize;
+}
+
+void CPreflightHandler::NSUserDefaultsPurge(const char* prefix)
+{
+ auto defaults = [NSUserDefaults standardUserDefaults];
+ NSDictionary<NSString*, id>* dict = [defaults dictionaryRepresentation];
+ NSString* aKey;
+ for (aKey in dict.allKeys)
+ {
+ if ([aKey hasPrefix:@(prefix)])
+ {
+ [defaults removeObjectForKey:aKey];
+ CLog::Log(LOGDEBUG, "nsuserdefaults: removing {}", aKey.UTF8String);
+ }
+ }
+}
+
+void CPreflightHandler::CheckForRemovedCacheFolder()
+{
+ // if already migrated, "UserdataMigrated" key will be in UserDefaults
+ // if user home directory does not exist, Apple deleted cache from under us.
+ // we will mark it for possible restore if the backup exist
+ auto defaults = [NSUserDefaults standardUserDefaults];
+ auto migration_key = @"UserdataMigrated";
+ if (![defaults objectForKey:migration_key])
+ {
+ // no existing data, no need to check any further.
+ return;
+ }
+
+ //!@todo: implement some sort of backup/restore if folder no longer exists?
+}
+
+void CPreflightHandler::MigrateUserdataXMLToNSUserDefaults()
+{
+ CLog::Log(LOGDEBUG,
+ "MigrateUserdataXMLToNSUserDefaults: "
+ "NSUserDefaultsSize({})",
+ NSUserDefaultsSize());
+
+ auto defaults = [NSUserDefaults standardUserDefaults];
+
+ // if already migrated, we are done.
+ auto migration_key = @"UserdataMigrated";
+ if ([defaults objectForKey:migration_key])
+ {
+ // If we require forced update of data from cache check for migration_key value
+ return;
+ }
+
+ CLog::Log(LOGDEBUG, "MigrateUserdataXMLToNSUserDefaults: migration starting");
+
+ NSUserDefaultsPurge("/userdata");
+ // now search for all xxx.xml files in the
+ // user home directory and copy them into NSUserDefaults
+ auto userHome = CTVOSFileUtils::GetUserHomeDirectory();
+ auto nsPath = @(userHome);
+
+ NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtPath:nsPath];
+ NSString* file;
+ while (file = [enumerator nextObject])
+ {
+ auto fullPath = [nsPath stringByAppendingPathComponent:file];
+
+ // skip the Thumbnails directory, there are no xml files present
+ // and it can be very large.
+ if ([fullPath containsString:@"Thumbnails"])
+ continue;
+
+ // check if it's a directory
+ BOOL isDirectory = NO;
+ [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
+ if (isDirectory)
+ continue;
+ // check if the file extension is 'xml'
+ if ([file.pathExtension isEqualToString:@"xml"])
+ {
+ // log what we are doing
+ CLog::Log(LOGDEBUG, "MigrateUserdataXMLToNSUserDefaults: Found -> {}}",
+ [fullPath UTF8String]);
+
+ // we cannot use a Cfile for src, it will get mapped into a CTVOSFile
+ const CURL srcUrl(fullPath.UTF8String);
+ XFILE::CPosixFile srcfile;
+ if (!srcfile.Open(srcUrl))
+ {
+ CLog::Log(LOGDEBUG, "MigrateUserdataXMLToNSUserDefaults: Failed opening file {}",
+ srcUrl.Get().c_str());
+ continue;
+ }
+
+ const CURL dtsUrl(fullPath.UTF8String);
+ XFILE::CTVOSFile dstfile;
+ if (dstfile.OpenForWrite(dtsUrl, true))
+ {
+ auto iBufferSize = 128 * 1024;
+ XFILE::auto_buffer buffer(iBufferSize);
+
+ while (true)
+ {
+ // read data
+ auto iread = srcfile.Read(buffer.get(), iBufferSize);
+ if (iread == 0)
+ break;
+ else if (iread < 0)
+ {
+ CLog::Log(LOGERROR, "MigrateUserdataXMLToNSUserDefaults: Failed read from file {}",
+ srcUrl.Get().c_str());
+ break;
+ }
+
+ // write data and make sure we write it all
+ auto iwrite = 0;
+ while (iwrite < iread)
+ {
+ auto iwrite2 = dstfile.Write(buffer.get() + iwrite, iread - iwrite);
+ if (iwrite2 <= 0)
+ break;
+ iwrite += iwrite2;
+ }
+
+ if (iwrite != iread)
+ {
+ CLog::Log(LOGERROR,
+ "MigrateUserdataXMLToNSUserDefaults: "
+ "Failed write to file {}",
+ dtsUrl.Get().c_str());
+ break;
+ }
+ }
+ dstfile.Close();
+ srcfile.Close();
+ srcfile.Delete(srcUrl);
+ }
+ }
+ }
+
+ // set the migration_key to a known value and write it.
+ [defaults setObject:@"1" forKey:migration_key];
+ [defaults synchronize];
+
+ CLog::Log(LOGDEBUG, "MigrateUserdataXMLToNSUserDefaults: migration finished");
+ CLog::Log(LOGDEBUG, "MigrateUserdataXMLToNSUserDefaults: NSUserDefaultsSize({})",
+ NSUserDefaultsSize());
+}
diff --git a/xbmc/platform/darwin/tvos/TVOSDisplayManager.h b/xbmc/platform/darwin/tvos/TVOSDisplayManager.h
new file mode 100644
index 0000000000..0d9cb39a0e
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSDisplayManager.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <CoreGraphics/CGBase.h>
+#import <CoreGraphics/CGGeometry.h>
+#import <Foundation/Foundation.h>
+
+@class CADisplayLink;
+
+class CWinSystemTVOS;
+
+@interface TVOSDisplayManager : NSObject
+{
+ CADisplayLink* m_displayLink;
+ CWinSystemTVOS* m_winSystem;
+ float m_displayRate;
+ CGSize m_screensize;
+}
+
+@property(readwrite) CGFloat screenScale;
+
+- (float)getDisplayRate;
+- (void)displayLinkTick:(CADisplayLink*)sender;
+- (void)displayRateSwitch:(float)refreshRate withDynamicRange:(int)dynamicRange;
+- (void)displayRateReset;
+- (void)removeModeSwitchObserver;
+- (void)addModeSwitchObserver;
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary*)change
+ context:(void*)context;
+- (const char*)stringFromDynamicRange:(int)dynamicRange;
+- (CGSize)getScreenSize;
+- (instancetype)init;
+@end
diff --git a/xbmc/platform/darwin/tvos/TVOSDisplayManager.mm b/xbmc/platform/darwin/tvos/TVOSDisplayManager.mm
new file mode 100644
index 0000000000..be413ff7af
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSDisplayManager.mm
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "TVOSDisplayManager.h"
+
+#include "ServiceBroker.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#import "windowing/tvos/WinSystemTVOS.h"
+
+#import "platform/darwin/tvos/TVOSEAGLView.h"
+#import "platform/darwin/tvos/XBMCController.h"
+
+#import <AVFoundation/AVDisplayCriteria.h>
+#import <AVKit/AVDisplayManager.h>
+#import <QuartzCore/CADisplayLink.h>
+
+#define DISPLAY_MODE_SWITCH_IN_PROGRESS NSStringFromSelector(@selector(displayModeSwitchInProgress))
+
+@interface AVDisplayCriteria ()
+@property(readonly) int videoDynamicRange;
+@property(readonly, nonatomic) float refreshRate;
+- (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2;
+@end
+
+@implementation TVOSDisplayManager
+
+@synthesize screenScale;
+
+#pragma mark - display switching routines
+- (float)getDisplayRate
+{
+ if (m_displayRate > 0.0f)
+ return m_displayRate;
+
+ return 60.0f;
+}
+
+- (void)displayLinkTick:(CADisplayLink*)sender
+{
+ auto duration = m_displayLink.duration;
+ if (duration > 0.0)
+ {
+ // we want fps, not duration in seconds.
+ m_displayRate = 1.0 / duration;
+ }
+}
+
+- (void)displayRateSwitch:(float)refreshRate withDynamicRange:(int)dynamicRange
+{
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) == ADJUST_REFRESHRATE_OFF)
+ return;
+ if (@available(tvOS 11.2, *))
+ {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ auto avDisplayManager = [g_xbmcController avDisplayManager];
+ if (refreshRate > 0.0f)
+ {
+ // videoDynamicRange values are based on watching
+ // console log when forcing different values.
+ // search for "Native Mode Requested" and pray :)
+ // searches for "FBSDisplayConfiguration" and "currentMode" will show the actual
+ // for example, currentMode = <FBSDisplayMode: 0x1c4298100; 1920x1080@2x (3840x2160/2) 24Hz p3 HDR10>
+ // SDR == 0, 1
+ // HDR == 2, 3
+ // DoblyVision == 4
+ auto displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:refreshRate
+ videoDynamicRange:dynamicRange];
+ // setting preferredDisplayCriteria will trigger a display rate switch
+ [self setDisplayCriteria:avDisplayManager displayCriteria:displayCriteria];
+ }
+ else
+ {
+ // switch back to tvOS defined user settings if we get
+ // zero or less than value for refreshRate. Should never happen :)
+ [self setDisplayCriteria:avDisplayManager displayCriteria:nil];
+ }
+ });
+ CLog::Log(LOGDEBUG, "displayRateSwitch request: refreshRate = {}, dynamicRange = {}",
+ refreshRate, [self stringFromDynamicRange:dynamicRange]);
+ }
+}
+
+- (void)displayRateReset
+{
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_VIDEOPLAYER_ADJUSTREFRESHRATE) == ADJUST_REFRESHRATE_OFF)
+ return;
+ if (@available(tvOS 11.2, *))
+ {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // setting preferredDisplayCriteria to nil will
+ // switch back to tvOS defined user settings
+ auto avDisplayManager = [g_xbmcController avDisplayManager];
+ [self setDisplayCriteria:avDisplayManager displayCriteria:nil];
+ });
+ }
+}
+
+- (void)setDisplayCriteria:(AVDisplayManager*)avDisplayManager
+ displayCriteria:(AVDisplayCriteria*)dispCriteria
+ __attribute__((availability(tvos, introduced = 11.2)))
+{
+ if (@available(tvOS 11.3, *))
+ {
+ if (avDisplayManager.displayCriteriaMatchingEnabled)
+ avDisplayManager.preferredDisplayCriteria = dispCriteria;
+ }
+ else
+ {
+ avDisplayManager.preferredDisplayCriteria = dispCriteria;
+ }
+}
+
+- (void)removeModeSwitchObserver
+{
+ if (@available(tvOS 11.2, *))
+ {
+ auto avDisplayManager = [g_xbmcController avDisplayManager];
+ [avDisplayManager removeObserver:self forKeyPath:DISPLAY_MODE_SWITCH_IN_PROGRESS];
+ }
+}
+
+- (void)addModeSwitchObserver
+{
+ if (@available(tvOS 11.2, *))
+ {
+ auto avDisplayManager = [g_xbmcController avDisplayManager];
+ [avDisplayManager addObserver:self
+ forKeyPath:DISPLAY_MODE_SWITCH_IN_PROGRESS
+ options:NSKeyValueObservingOptionNew
+ context:nullptr];
+ }
+}
+
+- (void)observeValueForKeyPath:(NSString*)keyPath
+ ofObject:(id)object
+ change:(NSDictionary*)change
+ context:(void*)context
+{
+ if (![keyPath isEqualToString:DISPLAY_MODE_SWITCH_IN_PROGRESS])
+ return;
+
+ // tracking displayModeSwitchInProgress via NSKeyValueObservingOptionNew,
+ // any changes in displayModeSwitchInProgress will fire this callback.
+ if (@available(tvOS 11.2, *))
+ {
+ std::string switchState = "NO";
+ int dynamicRange = 0;
+ float refreshRate;
+ auto avDisplayManager = [g_xbmcController avDisplayManager];
+ auto displayCriteria = avDisplayManager.preferredDisplayCriteria;
+ // preferredDisplayCriteria can be nil, this is NOT an error
+ // and just indicates tvOS defined user settings which we cannot see.
+ if (displayCriteria != nil)
+ {
+ refreshRate = displayCriteria.refreshRate;
+ dynamicRange = displayCriteria.videoDynamicRange;
+ }
+ if (avDisplayManager.displayModeSwitchInProgress)
+ {
+ switchState = "YES";
+ m_winSystem->AnnounceOnLostDevice();
+ m_winSystem->StartLostDeviceTimer();
+ }
+ else
+ {
+ switchState = "DONE";
+ m_winSystem->StartLostDeviceTimer();
+ m_winSystem->AnnounceOnLostDevice();
+ // displayLinkTick is tracking actual refresh duration.
+ // when isDisplayModeSwitchInProgress == NO, we have switched
+ // and stablized. We might have switched to some other
+ // rate than what we requested. setting preferredDisplayCriteria is
+ // only a request. For example, 30Hz might only be avaliable in HDR
+ // and asking for 30Hz/SDR might result in 60Hz/SDR and
+ // g_graphicsContext.SetFPS needs the actual refresh rate.
+ refreshRate = [self getDisplayRate];
+ }
+ //! @todo
+ //g_graphicsContext.SetFPS(refreshRate);
+ CLog::Log(LOGDEBUG, "displayModeSwitchInProgress = {}, refreshRate = {}, dynamicRange = {}",
+ switchState, refreshRate, [self stringFromDynamicRange:dynamicRange]);
+ }
+}
+
+- (const char*)stringFromDynamicRange:(int)dynamicRange
+{
+ switch (dynamicRange)
+ {
+ case 0 ... 1:
+ return "SDR";
+ case 2 ... 3:
+ return "HDR10";
+ case 4:
+ return "DolbyVision";
+ default:
+ return "Unknown";
+ }
+}
+
+- (CGSize)getScreenSize
+{
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ m_screensize.width = g_xbmcController.glView.bounds.size.width * self.screenScale;
+ m_screensize.height = g_xbmcController.glView.bounds.size.height * self.screenScale;
+ });
+ return m_screensize;
+}
+
+#pragma mark - init
+- (instancetype)init
+{
+ self = [super init];
+ if (!self)
+ return nil;
+
+ m_winSystem = dynamic_cast<CWinSystemTVOS*>(CServiceBroker::GetWinSystem());
+
+ m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
+ // we want the native cadence of the display hardware.
+ m_displayLink.preferredFramesPerSecond = 0;
+ [m_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
+
+ return self;
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/TVOSEAGLView.h b/xbmc/platform/darwin/tvos/TVOSEAGLView.h
new file mode 100644
index 0000000000..2bad0916a8
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSEAGLView.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <OpenGLES/EAGL.h>
+#import <OpenGLES/ES2/gl.h>
+#import <UIKit/UIKit.h>
+
+// This class wraps the CAEAGLLayer from CoreAnimation into a convenient UIView subclass.
+// The view content is basically an EAGL surface you render your OpenGL scene into.
+// Note that setting the view non-opaque will only work if the EAGL surface has an alpha channel.
+@interface TVOSEAGLView : UIView
+{
+ EAGLContext* m_context;
+ // The pixel dimensions of the CAEAGLLayer.
+ GLint m_framebufferWidth;
+ GLint m_framebufferHeight;
+ // The OpenGL ES names for the framebuffer and renderbuffer used to render to this view.
+ GLuint m_defaultFramebuffer, m_colorRenderbuffer, m_depthRenderbuffer;
+ UIScreen* m_currentScreen;
+ BOOL m_framebufferResizeRequested;
+}
+@property(readonly, getter=getCurrentEAGLContext) EAGLContext* m_context;
+@property(readonly, getter=getCurrentScreen) UIScreen* m_currentScreen;
+
+
+- (id)initWithFrame:(CGRect)frame withScreen:(UIScreen*)screen;
+- (void)setFramebuffer;
+- (bool)presentFramebuffer;
+- (CGFloat)getScreenScale:(UIScreen*)screen;
+
+@end
diff --git a/xbmc/platform/darwin/tvos/TVOSEAGLView.mm b/xbmc/platform/darwin/tvos/TVOSEAGLView.mm
new file mode 100644
index 0000000000..8bc8514b4d
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSEAGLView.mm
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "platform/darwin/tvos/TVOSEAGLView.h"
+
+#include "messaging/ApplicationMessenger.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/log.h"
+
+#import "platform/darwin/DarwinUtils.h"
+#import "platform/darwin/tvos/XBMCController.h"
+
+#import <signal.h>
+#import <stdio.h>
+
+#import "system.h"
+
+using namespace KODI::MESSAGING;
+
+@implementation TVOSEAGLView
+@synthesize m_context;
+@synthesize m_currentScreen;
+
++ (Class)layerClass
+{
+ return [CAEAGLLayer class];
+}
+
+- (id)initWithFrame:(CGRect)frame withScreen:(UIScreen*)screen
+{
+ m_framebufferResizeRequested = FALSE;
+ if ((self = [super initWithFrame:frame]))
+ {
+ auto scaleFactor = [self getScreenScale:UIScreen.mainScreen];
+
+ // Get the layer
+ auto eaglLayer = static_cast<CAEAGLLayer*>(self.layer);
+ eaglLayer.contentsScale = scaleFactor;
+ self.contentScaleFactor = scaleFactor;
+
+ eaglLayer.opaque = TRUE;
+ eaglLayer.drawableProperties = @{
+ kEAGLDrawablePropertyRetainedBacking : @NO,
+ kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
+ };
+
+ // Try OpenGL ES 3.0
+ auto aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
+
+ // Fallback to OpenGL ES 2.0
+ if (aContext == nullptr)
+ aContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+
+ if (!aContext)
+ CLog::Log(LOGERROR, "Failed to create ES context");
+ else if (![EAGLContext setCurrentContext:aContext])
+ CLog::Log(LOGERROR, "Failed to set ES context current");
+
+ m_context = aContext;
+
+ [self createFramebuffer];
+ [self setFramebuffer];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ [self deleteFramebuffer];
+}
+
+- (BOOL)canBecomeFocused
+{
+ // need this or we do not get GestureRecognizers under tvos.
+ return YES;
+}
+
+- (void)setContext:(EAGLContext*)newContext
+{
+ if (m_context != newContext)
+ {
+ [self deleteFramebuffer];
+ m_context = newContext;
+ [EAGLContext setCurrentContext:nil];
+ }
+}
+
+- (void)createFramebuffer
+{
+ if (m_context && !m_defaultFramebuffer)
+ {
+ [EAGLContext setCurrentContext:m_context];
+
+ // Create default framebuffer object.
+ glGenFramebuffers(1, &m_defaultFramebuffer);
+ glBindFramebuffer(GL_FRAMEBUFFER, m_defaultFramebuffer);
+
+ // Create color render buffer and allocate backing store.
+ glGenRenderbuffers(1, &m_colorRenderbuffer);
+ glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer);
+ [m_context renderbufferStorage:GL_RENDERBUFFER
+ fromDrawable:static_cast<CAEAGLLayer*>(self.layer)];
+ glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &m_framebufferWidth);
+ glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &m_framebufferHeight);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
+ m_colorRenderbuffer);
+
+ glGenRenderbuffers(1, &m_depthRenderbuffer);
+ glBindRenderbuffer(GL_RENDERBUFFER, m_depthRenderbuffer);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, m_framebufferWidth,
+ m_framebufferHeight);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER,
+ m_depthRenderbuffer);
+
+ if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
+ CLog::Log(LOGERROR, "Failed to make complete framebuffer object {}",
+ glCheckFramebufferStatus(GL_FRAMEBUFFER));
+ }
+}
+
+- (void)deleteFramebuffer
+{
+ if (m_context)
+ {
+ [EAGLContext setCurrentContext:m_context];
+
+ if (m_defaultFramebuffer)
+ {
+ glDeleteFramebuffers(1, &m_defaultFramebuffer);
+ m_defaultFramebuffer = 0;
+ }
+
+ if (m_colorRenderbuffer)
+ {
+ glDeleteRenderbuffers(1, &m_colorRenderbuffer);
+ m_colorRenderbuffer = 0;
+ }
+
+ if (m_depthRenderbuffer)
+ {
+ glDeleteRenderbuffers(1, &m_depthRenderbuffer);
+ m_depthRenderbuffer = 0;
+ }
+ }
+}
+
+- (void)setFramebuffer
+{
+ if (m_context)
+ {
+ if ([EAGLContext currentContext] != m_context)
+ [EAGLContext setCurrentContext:m_context];
+
+ glBindFramebuffer(GL_FRAMEBUFFER, m_defaultFramebuffer);
+
+ if (m_framebufferHeight > m_framebufferWidth)
+ {
+ glViewport(0, 0, m_framebufferHeight, m_framebufferWidth);
+ glScissor(0, 0, m_framebufferHeight, m_framebufferWidth);
+ }
+ else
+ {
+ glViewport(0, 0, m_framebufferWidth, m_framebufferHeight);
+ glScissor(0, 0, m_framebufferWidth, m_framebufferHeight);
+ }
+ }
+}
+
+- (bool)presentFramebuffer
+{
+ bool success = FALSE;
+ if (m_context)
+ {
+ if ([EAGLContext currentContext] != m_context)
+ [EAGLContext setCurrentContext:m_context];
+
+ glBindRenderbuffer(GL_RENDERBUFFER, m_colorRenderbuffer);
+ success = [m_context presentRenderbuffer:GL_RENDERBUFFER];
+ }
+
+ return success;
+}
+
+- (CGFloat)getScreenScale:(UIScreen*)screen
+{
+ return screen.scale;
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/TVOSKeyboardView.h b/xbmc/platform/darwin/tvos/TVOSKeyboardView.h
new file mode 100644
index 0000000000..ef9ec63803
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSKeyboardView.h
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "platform/darwin/ios-common/DarwinEmbedKeyboardView.h"
+
+@interface TVOSKeyboardView : KeyboardView
+@end
diff --git a/xbmc/platform/darwin/tvos/TVOSKeyboardView.mm b/xbmc/platform/darwin/tvos/TVOSKeyboardView.mm
new file mode 100644
index 0000000000..881166749e
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSKeyboardView.mm
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "TVOSKeyboardView.h"
+
+@implementation TVOSKeyboardView
+@end
diff --git a/xbmc/platform/darwin/tvos/TVOSSettingsHandler.h b/xbmc/platform/darwin/tvos/TVOSSettingsHandler.h
new file mode 100644
index 0000000000..e066cf3e62
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSSettingsHandler.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "settings/lib/ISettingCallback.h"
+
+class CTVOSInputSettings : public ISettingCallback
+{
+public:
+ static CTVOSInputSettings& GetInstance();
+
+ void Initialize();
+
+ virtual void OnSettingChanged(std::shared_ptr<const CSetting> setting) override;
+
+private:
+ CTVOSInputSettings() = default;
+ CTVOSInputSettings(CTVOSInputSettings const&);
+ CTVOSInputSettings& operator=(CTVOSInputSettings const&);
+
+ static CTVOSInputSettings* m_instance;
+};
diff --git a/xbmc/platform/darwin/tvos/TVOSSettingsHandler.mm b/xbmc/platform/darwin/tvos/TVOSSettingsHandler.mm
new file mode 100644
index 0000000000..4e977a88dd
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSSettingsHandler.mm
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "TVOSSettingsHandler.h"
+
+#include "ServiceBroker.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "threads/Atomics.h"
+
+#import "platform/darwin/tvos/XBMCController.h"
+#import "platform/darwin/tvos/input/LibInputHandler.h"
+#import "platform/darwin/tvos/input/LibInputSettings.h"
+
+#import "system.h"
+
+static std::atomic_flag sg_singleton_lock_variable = ATOMIC_FLAG_INIT;
+CTVOSInputSettings* CTVOSInputSettings::m_instance = nullptr;
+
+CTVOSInputSettings& CTVOSInputSettings::GetInstance()
+{
+ CAtomicSpinLock lock(sg_singleton_lock_variable);
+ if (!m_instance)
+ m_instance = new CTVOSInputSettings();
+
+ return *m_instance;
+}
+
+void CTVOSInputSettings::Initialize()
+{
+ bool enable = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_APPLESIRI);
+ g_xbmcController.inputHandler.inputSettings.useSiriRemote = enable;
+ bool enableTimeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_APPLESIRITIMEOUTENABLED);
+ [g_xbmcController.inputHandler.inputSettings setRemoteIdleEnabled:enableTimeout];
+ int timeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_INPUT_APPLESIRITIMEOUT);
+ [g_xbmcController.inputHandler.inputSettings setRemoteIdleTimeout:timeout];
+}
+
+void CTVOSInputSettings::OnSettingChanged(std::shared_ptr<const CSetting> setting)
+{
+ if (setting == nullptr)
+ return;
+
+ const std::string& settingId = setting->GetId();
+ if (settingId == CSettings::SETTING_INPUT_APPLESIRI)
+ {
+ bool enable = std::dynamic_pointer_cast<const CSettingBool>(setting)->GetValue();
+ g_xbmcController.inputHandler.inputSettings.useSiriRemote = enable;
+ }
+ else if (settingId == CSettings::SETTING_INPUT_APPLESIRITIMEOUTENABLED)
+ {
+ bool enableTimeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
+ CSettings::SETTING_INPUT_APPLESIRITIMEOUTENABLED);
+ [g_xbmcController.inputHandler.inputSettings setRemoteIdleEnabled:enableTimeout];
+ }
+ else if (settingId == CSettings::SETTING_INPUT_APPLESIRITIMEOUT)
+ {
+ int timeout = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(
+ CSettings::SETTING_INPUT_APPLESIRITIMEOUT);
+ [g_xbmcController.inputHandler.inputSettings setRemoteIdleTimeout:timeout];
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/TVOSTopShelf.h b/xbmc/platform/darwin/tvos/TVOSTopShelf.h
new file mode 100644
index 0000000000..ee5c244a5e
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSTopShelf.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "FileItem.h"
+#include "threads/CriticalSection.h"
+
+class CTVOSTopShelf
+{
+public:
+ static CTVOSTopShelf& GetInstance();
+ void RunTopShelf();
+ void SetTopShelfItems(CFileItemList& movies, CFileItemList& tv);
+ void HandleTopShelfUrl(const std::string& url, const bool run);
+
+private:
+ CTVOSTopShelf() = default;
+ ~CTVOSTopShelf() = default;
+
+ static std::string m_url;
+ static bool m_handleUrl;
+};
diff --git a/xbmc/platform/darwin/tvos/TVOSTopShelf.mm b/xbmc/platform/darwin/tvos/TVOSTopShelf.mm
new file mode 100644
index 0000000000..a3816f673f
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TVOSTopShelf.mm
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+
+#import "TVOSTopShelf.h"
+
+#include "Application.h"
+#include "DatabaseManager.h"
+#include "FileItem.h"
+#include "filesystem/File.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "messaging/ApplicationMessenger.h"
+#include "tvosShared.h"
+#include "utils/Base64.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+#include "video/VideoDatabase.h"
+#include "video/VideoInfoTag.h"
+#include "video/VideoThumbLoader.h"
+#include "video/dialogs/GUIDialogVideoInfo.h"
+#include "video/windows/GUIWindowVideoBase.h"
+#include "video/windows/GUIWindowVideoNav.h"
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+#import <mach/mach_host.h>
+#import <sys/sysctl.h>
+
+#import "system.h"
+
+static const int MaxItems = 5;
+
+std::string CTVOSTopShelf::m_url;
+bool CTVOSTopShelf::m_handleUrl;
+
+CTVOSTopShelf& CTVOSTopShelf::GetInstance()
+{
+ static CTVOSTopShelf sTopShelf;
+ return sTopShelf;
+}
+
+void CTVOSTopShelf::SetTopShelfItems(CFileItemList& movies, CFileItemList& tv)
+{
+ @autoreleasepool
+ {
+ auto storeUrl = [tvosShared getSharedURL];
+ if (!storeUrl)
+ return;
+
+ storeUrl = [storeUrl URLByAppendingPathComponent:@"RA" isDirectory:YES];
+ const BOOL isJailbroken = [tvosShared isJailbroken];
+ CLog::Log(LOGDEBUG, "TopShelf: using shared path {} (jailbroken: {})\n",
+ storeUrl.path.UTF8String, isJailbroken ? "yes" : "no");
+
+ auto sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:[tvosShared getSharedID]];
+ auto sharedDictJailbreak = isJailbroken ? [[NSMutableDictionary alloc] initWithCapacity:2 + 2]
+ : nil; // for jailbroken devices
+
+ // store all old thumbs in array
+ auto fileManager = NSFileManager.defaultManager;
+ auto filePaths =
+ [NSMutableSet setWithArray:[fileManager contentsOfDirectoryAtPath:storeUrl.path error:nil]];
+ std::string raPath = storeUrl.path.UTF8String;
+ CVideoThumbLoader thumbLoader;
+
+ auto fillSharedDicts =
+ [&](CFileItemList& videoItems, NSString* videosKey, NSString* videosTitleKey,
+ uint32_t titleStringCode,
+ std::function<std::string(CFileItemPtr videoItem)> getThumbnailForItem,
+ std::function<std::string(CFileItemPtr videoItem)> getTitleForItem) {
+ if (videoItems.Size() <= 0)
+ {
+ // cleanup if there is no RA
+ [sharedDefaults removeObjectForKey:videosKey];
+ [sharedDefaults removeObjectForKey:videosTitleKey];
+ return;
+ }
+
+ const int topShelfSize = std::min(videoItems.Size(), MaxItems);
+ auto videosArray = [NSMutableArray arrayWithCapacity:topShelfSize];
+ for (int i = 0; i < topShelfSize; ++i)
+ {
+ @autoreleasepool
+ {
+ auto videoItem = videoItems.Get(i);
+ if (!videoItem->HasArt("thumb"))
+ thumbLoader.LoadItem(videoItem.get());
+
+ auto thumbnailPath = getThumbnailForItem(videoItem);
+ auto fileName = std::to_string(videoItem->GetVideoInfoTag()->m_iDbId) +
+ URIUtils::GetFileName(thumbnailPath);
+ auto destPath = URIUtils::AddFileToFolder(raPath, fileName);
+ if (!XFILE::CFile::Exists(destPath))
+ XFILE::CFile::Copy(thumbnailPath, destPath);
+ else
+ {
+ // remove from array so it doesnt get deleted at the end
+ [filePaths removeObject:@(fileName.c_str())];
+ }
+
+ auto title = getTitleForItem(videoItem);
+ CLog::Log(LOGDEBUG, "TopShelf: - adding video to '{}' array: {}\n",
+ videosKey.UTF8String, title.c_str());
+ [videosArray addObject:@{
+ @"title" : @(title.c_str()),
+ @"thumb" : @(fileName.c_str()),
+ @"url" : @(Base64::Encode(videoItem->GetVideoInfoTag()->GetPath()).c_str())
+ }];
+ }
+ }
+ [sharedDefaults setObject:videosArray forKey:videosKey];
+ sharedDictJailbreak[videosKey] = videosArray;
+
+ auto tvTitle = @(g_localizeStrings.Get(titleStringCode).c_str());
+ [sharedDefaults setObject:tvTitle forKey:videosTitleKey];
+ sharedDictJailbreak[videosTitleKey] = tvTitle;
+ };
+
+ fillSharedDicts(movies, @"movies", @"moviesTitle", 20386,
+ [](CFileItemPtr videoItem) { return videoItem->GetArt("thumb"); },
+ [](CFileItemPtr videoItem) { return videoItem->GetLabel(); });
+
+ CVideoDatabase videoDb;
+ videoDb.Open();
+ fillSharedDicts(tv, @"tv", @"tvTitle", 20387,
+ [&videoDb](CFileItemPtr videoItem) {
+ int season = videoItem->GetVideoInfoTag()->m_iIdSeason;
+ return season > 0 ? videoDb.GetArtForItem(season, MediaTypeSeason, "poster")
+ : std::string{};
+ },
+ [](CFileItemPtr videoItem) {
+ return StringUtils::Format(
+ "%s s%02de%02d", videoItem->GetVideoInfoTag()->m_strShowTitle.c_str(),
+ videoItem->GetVideoInfoTag()->m_iSeason,
+ videoItem->GetVideoInfoTag()->m_iEpisode);
+ });
+ videoDb.Close();
+
+ // remove unused thumbs from cache folder
+ NSString* strFiles;
+ for (strFiles in filePaths)
+ [fileManager removeItemAtURL:[storeUrl URLByAppendingPathComponent:strFiles isDirectory:NO]
+ error:nil];
+
+ [sharedDictJailbreak writeToURL:[storeUrl URLByAppendingPathComponent:@"shared.dict"]
+ atomically:YES];
+ [sharedDefaults synchronize];
+ }
+}
+
+void CTVOSTopShelf::RunTopShelf()
+{
+ if (!m_handleUrl)
+ return;
+
+ m_handleUrl = false;
+ std::vector<std::string> split = StringUtils::Split(m_url, "/");
+ std::string url = Base64::Decode(split[4]);
+
+ //!@Todo handle tvcontentitem.displayurl. Should show topshelf item video info
+ // check split[2] for url type (display or play)
+
+ // its a bit ugly, but only way to get resume window to show
+ std::string cmd =
+ StringUtils::Format("PlayMedia(%s)", StringUtils::Paramify(url.c_str()).c_str());
+ KODI::MESSAGING::CApplicationMessenger::GetInstance().PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1,
+ nullptr, cmd);
+}
+
+void CTVOSTopShelf::HandleTopShelfUrl(const std::string& url, const bool run)
+{
+ m_url = url;
+ m_handleUrl = run;
+}
diff --git a/xbmc/platform/darwin/tvos/TopShelf/Info.plist.in b/xbmc/platform/darwin/tvos/TopShelf/Info.plist.in
new file mode 100644
index 0000000000..18254f034a
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TopShelf/Info.plist.in
@@ -0,0 +1,44 @@
+<?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>TopShelf</string>
+ <key>CFBundleExecutable</key>
+ <string>@TOPSHELF_EXTENSION_NAME@</string>
+ <key>CFBundleIdentifier</key>
+ <string>@PLATFORM_BUNDLE_IDENTIFIER@.topshelf</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>TopShelf</string>
+ <key>CFBundlePackageType</key>
+ <string>XPC!</string>
+ <key>CFBundleShortVersionString</key>
+ <string>@APP_VERSION@</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>@APP_SCMID@</string>
+ <key>NSExtension</key>
+ <dict>
+ <key>NSExtensionAttributes</key>
+ <dict>
+ <key>TVExtensionProtocols</key>
+ <array>
+ <string>TVTopShelfProvider</string>
+ </array>
+ </dict>
+ <key>NSExtensionPointIdentifier</key>
+ <string>com.apple.tv-services</string>
+ <key>NSExtensionPrincipalClass</key>
+ <string>ServiceProvider</string>
+ </dict>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+</dict>
+</plist>
diff --git a/xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.h b/xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.h
new file mode 100644
index 0000000000..016629bff0
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 Team MrMC
+ * https://github.com/MrMC
+ *
+ * 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 2, 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 MrMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#import <Foundation/Foundation.h>
+#import <TVServices/TVServices.h>
+
+@interface ServiceProvider : NSObject <TVTopShelfProvider>
+@end
diff --git a/xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.m b/xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.m
new file mode 100644
index 0000000000..3277db4e49
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TopShelf/ServiceProvider.m
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 Team MrMC
+ * https://github.com/MrMC
+ *
+ * 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 2, 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 MrMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#import "ServiceProvider.h"
+
+#import "../tvosShared.h"
+
+@implementation ServiceProvider
+
+#pragma mark - TVTopShelfProvider protocol
+
+- (TVTopShelfContentStyle)topShelfStyle
+{
+ return TVTopShelfContentStyleSectioned;
+}
+
+- (NSArray<TVContentItem*>*)topShelfItems
+{
+ __auto_type storeUrl = [tvosShared getSharedURL];
+ if (!storeUrl)
+ return @[];
+
+ __auto_type const sharedID = [tvosShared getSharedID];
+ __auto_type const shared = [[NSUserDefaults alloc] initWithSuiteName:sharedID];
+ __auto_type topShelfItems = [[NSMutableArray alloc] init];
+ __auto_type wrapperIdentifier =
+ [[TVContentIdentifier alloc] initWithIdentifier:@"shelf-wrapper" container:nil];
+
+ NSArray* movieArray = nil;
+ NSArray* tvArray = nil;
+ NSDictionary* sharedDict = nil;
+
+ if ([tvosShared isJailbroken])
+ {
+ __auto_type sharedDictUrl =
+ [storeUrl URLByAppendingPathComponent:@"shared.dict" isDirectory:NO];
+ sharedDict = [NSDictionary dictionaryWithContentsOfFile:[sharedDictUrl path]];
+
+ movieArray = [sharedDict valueForKey:@"movies"];
+ tvArray = [sharedDict valueForKey:@"tv"];
+ }
+ else
+ {
+ movieArray = [shared objectForKey:@"movies"];
+ tvArray = [shared valueForKey:@"tv"];
+ }
+
+ __auto_type mainAppBundle = [tvosShared mainAppBundle];
+ __auto_type kodiUrlScheme = @"kodi"; // fallback value
+ NSDictionary* dic;
+ for (dic in [mainAppBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"])
+ {
+ if ([dic[@"CFBundleURLName"] isEqualToString:mainAppBundle.bundleIdentifier])
+ {
+ kodiUrlScheme = dic[@"CFBundleURLSchemes"][0];
+ break;
+ }
+ }
+
+ storeUrl = [storeUrl URLByAppendingPathComponent:@"RA" isDirectory:YES];
+ __auto_type contentItemsFrom = ^NSArray<TVContentItem*>*(NSArray* videosArray)
+ {
+ NSMutableArray<TVContentItem*>* contentItems =
+ [[NSMutableArray alloc] initWithCapacity:videosArray.count];
+ NSDictionary* videoDict;
+ for (videoDict in videosArray)
+ {
+ __auto_type identifier =
+ [[TVContentIdentifier alloc] initWithIdentifier:@"VOD" container:wrapperIdentifier];
+ __auto_type contentItem = [[TVContentItem alloc] initWithContentIdentifier:identifier];
+
+ [contentItem
+ setImageURL:[storeUrl URLByAppendingPathComponent:[videoDict valueForKey:@"thumb"]
+ isDirectory:NO]
+ forTraits:TVContentItemImageTraitScreenScale1x];
+ contentItem.imageShape = TVContentItemImageShapePoster;
+ contentItem.title = [videoDict valueForKey:@"title"];
+ NSString* url = [videoDict valueForKey:@"url"];
+ contentItem.displayURL = [NSURL
+ URLWithString:[NSString stringWithFormat:@"%@://display/movie/%@", kodiUrlScheme, url]];
+ contentItem.playURL = [NSURL
+ URLWithString:[NSString stringWithFormat:@"%@://play/movie/%@", kodiUrlScheme, url]];
+ [contentItems addObject:contentItem];
+ }
+ return contentItems;
+ };
+
+ if ([movieArray count] > 0)
+ {
+ __auto_type itemMovie = [[TVContentItem alloc] initWithContentIdentifier:wrapperIdentifier];
+ itemMovie.title = [(sharedDict ?: shared) valueForKey:@"moviesTitle"];
+ itemMovie.topShelfItems = contentItemsFrom(movieArray);
+ [topShelfItems addObject:itemMovie];
+ }
+
+ if ([tvArray count] > 0)
+ {
+ __auto_type itemTv = [[TVContentItem alloc] initWithContentIdentifier:wrapperIdentifier];
+ itemTv.title = [(sharedDict ?: shared) valueForKey:@"tvTitle"];
+ itemTv.topShelfItems = contentItemsFrom(tvArray);
+ [topShelfItems addObject:itemTv];
+ }
+
+ return topShelfItems;
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/TopShelf/TopShelf.entitlements.in b/xbmc/platform/darwin/tvos/TopShelf/TopShelf.entitlements.in
new file mode 100644
index 0000000000..70e501bca5
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/TopShelf/TopShelf.entitlements.in
@@ -0,0 +1,10 @@
+<?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>com.apple.security.application-groups</key>
+ <array>
+ <string>group.@PLATFORM_BUNDLE_IDENTIFIER@</string>
+ </array>
+</dict>
+</plist>
diff --git a/xbmc/platform/darwin/tvos/XBMCApplication.h b/xbmc/platform/darwin/tvos/XBMCApplication.h
new file mode 100644
index 0000000000..78a5ff9009
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/XBMCApplication.h
@@ -0,0 +1,13 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface XBMCApplicationDelegate : UIResponder <UIApplicationDelegate>
+@property(nullable, nonatomic, strong) UIWindow* window;
+@end
diff --git a/xbmc/platform/darwin/tvos/XBMCApplication.mm b/xbmc/platform/darwin/tvos/XBMCApplication.mm
new file mode 100644
index 0000000000..a267b67a90
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/XBMCApplication.mm
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "platform/darwin/tvos/XBMCApplication.h"
+
+#import "platform/darwin/NSLogDebugHelpers.h"
+#import "platform/darwin/tvos/PreflightHandler.h"
+#import "platform/darwin/tvos/TVOSTopShelf.h"
+#import "platform/darwin/tvos/XBMCController.h"
+
+#import <AVFoundation/AVFoundation.h>
+
+@implementation XBMCApplicationDelegate
+
+- (XBMCController*)xbmcController
+{
+ return static_cast<XBMCController*>(self.window.rootViewController);
+}
+
+#pragma mark - Shutdown Procedures
+
+- (void)applicationWillResignActive:(UIApplication*)application
+{
+ [self.xbmcController pauseAnimation];
+ [self.xbmcController becomeInactive];
+}
+
+- (void)applicationDidEnterBackground:(UIApplication*)application
+{
+ if (application.applicationState == UIApplicationStateBackground)
+ {
+ // the app is turn into background, not in by screen lock which has app state inactive.
+ [self.xbmcController enterBackground];
+ }
+}
+
+- (void)applicationWillTerminate:(UIApplication*)application
+{
+ [self.xbmcController stopAnimation];
+}
+
+#pragma mark - Startup Procedures
+
+- (void)applicationDidBecomeActive:(UIApplication*)application
+{
+ [self.xbmcController resumeAnimation];
+ [self.xbmcController enterForeground];
+}
+
+- (BOOL)application:(UIApplication*)application
+ didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
+{
+ // check if apple removed our Cache folder first
+ // this will trigger the restore if there is a backup available
+ CPreflightHandler::CheckForRemovedCacheFolder();
+
+ // This needs to run before anything does any CLog::Log calls
+ // as they will directly cause guisetting to get accessed/created
+ // via debug log settings.
+ CPreflightHandler::MigrateUserdataXMLToNSUserDefaults();
+
+ // UI setup
+ self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
+ self.window.rootViewController = [XBMCController new];
+ [self.window makeKeyAndVisible];
+ [self.xbmcController startAnimation];
+
+ // audio session setup
+ auto audioSession = AVAudioSession.sharedInstance;
+ NSError* err = nil;
+ if (![audioSession setCategory:AVAudioSessionCategoryPlayback error:&err])
+ NSLog(@"audioSession setCategory failed: %@", err);
+
+ err = nil;
+ if (![audioSession setMode:AVAudioSessionModeMoviePlayback error:&err])
+ NSLog(@"audioSession setMode failed: %@", err);
+
+ err = nil;
+ if (![audioSession setActive:YES error:&err])
+ NSLog(@"audioSession setActive failed: %@", err);
+
+ return YES;
+}
+
+- (BOOL)application:(UIApplication*)app
+ openURL:(NSURL*)url
+ options:(NSDictionary<NSString*, id>*)options
+{
+ NSArray* urlComponents = [url.absoluteString componentsSeparatedByString:@"/"];
+ NSString* action = urlComponents[2];
+ if ([action isEqualToString:@"display"] || [action isEqualToString:@"play"])
+ CTVOSTopShelf::GetInstance().HandleTopShelfUrl(url.absoluteString.UTF8String, true);
+ return YES;
+}
+@end
+
+static void SigPipeHandler(int s)
+{
+ NSLog(@"We Got a Pipe Signal: %d____________", s);
+}
+
+int main(int argc, char* argv[])
+{
+ @autoreleasepool
+ {
+ signal(SIGPIPE, SigPipeHandler);
+
+ int retVal = 0;
+ @try
+ {
+ retVal =
+ UIApplicationMain(argc, argv, nil, NSStringFromClass([XBMCApplicationDelegate class]));
+ }
+ @catch (id theException)
+ {
+ ELOG(@"%@", theException);
+ }
+ @finally
+ {
+ ILOG(@"This always happens.");
+ }
+
+ return retVal;
+ }
+}
diff --git a/xbmc/platform/darwin/tvos/XBMCController.h b/xbmc/platform/darwin/tvos/XBMCController.h
new file mode 100644
index 0000000000..2d474f8281
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/XBMCController.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include <memory>
+#include <string>
+
+#import <Foundation/Foundation.h>
+#import <OpenGLES/EAGL.h>
+#import <UIKit/UIKit.h>
+
+@class AVDisplayManager;
+@class DarwinEmbedNowPlayingInfoManager;
+@class TVOSEAGLView;
+@class TVOSLibInputHandler;
+@class TVOSDisplayManager;
+
+class CFileItem;
+
+@interface XBMCController : UIViewController
+{
+ BOOL m_isPlayingBeforeInactive;
+ UIBackgroundTaskIdentifier m_bgTask;
+ bool m_nativeKeyboardActive;
+ BOOL m_pause;
+ BOOL m_animating;
+ NSConditionLock* m_animationThreadLock;
+ NSThread* m_animationThread;
+}
+
+@property(nonatomic) BOOL appAlive;
+@property(nonatomic, strong) DarwinEmbedNowPlayingInfoManager* MPNPInfoManager;
+@property(nonatomic, strong) TVOSDisplayManager* displayManager;
+@property(nonatomic, strong) TVOSEAGLView* glView;
+@property(nonatomic, strong) TVOSLibInputHandler* inputHandler;
+
+- (void)pauseAnimation;
+- (void)resumeAnimation;
+- (void)startAnimation;
+- (void)stopAnimation;
+
+- (void)enterBackground;
+- (void)enterForeground;
+- (void)becomeInactive;
+- (void)setFramebuffer;
+- (bool)presentFramebuffer;
+- (void)activateKeyboard:(UIView*)view;
+- (void)deactivateKeyboard:(UIView*)view;
+- (void)nativeKeyboardActive:(bool)active;
+
+- (void)enableBackGroundTask;
+- (void)disableBackGroundTask;
+
+- (void)disableScreenSaver;
+- (void)enableScreenSaver;
+- (bool)resetSystemIdleTimer;
+
+- (CGRect)fullscreenSubviewFrame;
+
+- (AVDisplayManager*)avDisplayManager __attribute__((availability(tvos, introduced = 11.2)));
+
+- (EAGLContext*)getEAGLContextObj;
+
+@end
+
+extern XBMCController* g_xbmcController;
diff --git a/xbmc/platform/darwin/tvos/XBMCController.mm b/xbmc/platform/darwin/tvos/XBMCController.mm
new file mode 100644
index 0000000000..7a8153383b
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/XBMCController.mm
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "platform/darwin/tvos/XBMCController.h"
+
+#include "AppParamParser.h"
+#include "Application.h"
+#include "CompileInfo.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "interfaces/AnnouncementManager.h"
+#include "messaging/ApplicationMessenger.h"
+#include "network/NetworkServices.h"
+#include "platform/xbmc.h"
+#include "settings/AdvancedSettings.h"
+#include "settings/SettingsComponent.h"
+#include "utils/log.h"
+#import "windowing/tvos/WinEventsTVOS.h"
+#import "windowing/tvos/WinSystemTVOS.h"
+
+#import "platform/darwin/ios-common/AnnounceReceiver.h"
+#import "platform/darwin/ios-common/DarwinEmbedNowPlayingInfoManager.h"
+#import "platform/darwin/tvos/TVOSDisplayManager.h"
+#import "platform/darwin/tvos/TVOSEAGLView.h"
+#import "platform/darwin/tvos/TVOSTopShelf.h"
+#import "platform/darwin/tvos/XBMCApplication.h"
+#import "platform/darwin/tvos/input/LibInputHandler.h"
+#import "platform/darwin/tvos/input/LibInputRemote.h"
+#import "platform/darwin/tvos/input/LibInputTouch.h"
+
+#import <AVKit/AVDisplayManager.h>
+#import <AVKit/UIWindow.h>
+
+#import "system.h"
+
+using namespace KODI::MESSAGING;
+
+XBMCController* g_xbmcController;
+
+#pragma mark - XBMCController implementation
+@implementation XBMCController
+
+@synthesize appAlive = m_appAlive;
+@synthesize MPNPInfoManager;
+@synthesize displayManager;
+@synthesize inputHandler;
+@synthesize glView;
+
+#pragma mark - UIView Keyboard
+
+- (void)activateKeyboard:(UIView*)view
+{
+ [self.view addSubview:view];
+ glView.userInteractionEnabled = NO;
+}
+
+- (void)deactivateKeyboard:(UIView*)view
+{
+ [view removeFromSuperview];
+ glView.userInteractionEnabled = YES;
+ [self becomeFirstResponder];
+}
+
+- (void)nativeKeyboardActive:(bool)active;
+{
+ // Not used on tvOS
+}
+
+#pragma mark - View
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+
+ glView = [[TVOSEAGLView alloc] initWithFrame:self.view.bounds withScreen:[UIScreen mainScreen]];
+
+ // Check if screen is Retina
+ displayManager.screenScale = [glView getScreenScale:[UIScreen mainScreen]];
+
+ self.view.backgroundColor = UIColor.blackColor;
+ [self.view addSubview:glView];
+
+ [inputHandler.inputTouch createSwipeGestureRecognizers];
+ [inputHandler.inputTouch createPanGestureRecognizers];
+ [inputHandler.inputTouch createPressGesturecognizers];
+ [inputHandler.inputTouch createTapGesturecognizers];
+
+ [displayManager addModeSwitchObserver];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+ [self resumeAnimation];
+ [super viewWillAppear:animated];
+}
+
+- (void)viewDidAppear:(BOOL)animated
+{
+ [super viewDidAppear:animated];
+ [self becomeFirstResponder];
+ [[UIApplication sharedApplication]
+ beginReceivingRemoteControlEvents]; // @todo MPRemoteCommandCenter
+}
+
+- (void)viewWillDisappear:(BOOL)animated
+{
+ [self pauseAnimation];
+ [super viewWillDisappear:animated];
+}
+
+- (void)viewDidUnload
+{
+ [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
+ [self resignFirstResponder];
+ [super viewDidUnload];
+}
+
+- (UIView*)inputView
+{
+ // override our input view to an empty view
+ // this prevents the on screen keyboard
+ // which would be shown whenever this UIResponder
+ // becomes the first responder (which is always the case!)
+ // caused by implementing the UIKeyInput protocol
+ return [[UIView alloc] initWithFrame:CGRectZero];
+}
+
+#pragma mark - FirstResponder
+
+- (BOOL)canBecomeFirstResponder
+{
+ return YES;
+}
+
+#pragma mark - FrameBuffer
+
+- (void)setFramebuffer
+{
+ if (!m_pause)
+ [glView setFramebuffer];
+}
+
+- (bool)presentFramebuffer
+{
+ if (!m_pause)
+ return [glView presentFramebuffer];
+ else
+ return FALSE;
+}
+
+- (CGRect)fullscreenSubviewFrame
+{
+ return UIScreen.mainScreen.bounds;
+}
+
+- (void)didReceiveMemoryWarning
+{
+ // Releases the view if it doesn't have a superview.
+ [super didReceiveMemoryWarning];
+ // Release any cached data, images, etc. that aren't in use.
+}
+
+#pragma mark - BackgroundTask
+
+- (void)enableBackGroundTask
+{
+ if (m_bgTask != UIBackgroundTaskInvalid)
+ {
+ [[UIApplication sharedApplication] endBackgroundTask:m_bgTask];
+ m_bgTask = UIBackgroundTaskInvalid;
+ }
+ CLog::Log(LOGDEBUG, "%s: beginBackgroundTask", __PRETTY_FUNCTION__);
+ // we have to alloc the background task for keep network working after screen lock and dark.
+ m_bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
+}
+
+- (void)disableBackGroundTask
+{
+ if (m_bgTask != UIBackgroundTaskInvalid)
+ {
+ CLog::Log(LOGDEBUG, "%s: endBackgroundTask", __PRETTY_FUNCTION__);
+ [[UIApplication sharedApplication] endBackgroundTask:m_bgTask];
+ m_bgTask = UIBackgroundTaskInvalid;
+ }
+}
+
+#pragma mark - AppFocus
+
+- (void)becomeInactive
+{
+ // if we were interrupted, already paused here
+ // else if user background us or lock screen, only pause video here, audio keep playing.
+ if (g_application.GetAppPlayer().IsPlayingVideo() && !g_application.GetAppPlayer().IsPaused())
+ {
+ m_isPlayingBeforeInactive = YES;
+ CApplicationMessenger::GetInstance().SendMsg(TMSG_MEDIA_PAUSE_IF_PLAYING);
+ }
+}
+
+- (void)enterBackground
+{
+ // We have 5 seconds before the OS will force kill us for delaying too long.
+ XbmcThreads::EndTime timer(4500);
+
+ // this should not be required as we 'should' get becomeInactive before enterBackground
+ if (g_application.GetAppPlayer().IsPlaying() && !g_application.GetAppPlayer().IsPaused())
+ {
+ m_isPlayingBeforeInactive = YES;
+ CApplicationMessenger::GetInstance().SendMsg(TMSG_MEDIA_PAUSE_IF_PLAYING);
+ }
+
+ CWinSystemTVOS* winSystem = dynamic_cast<CWinSystemTVOS*>(CServiceBroker::GetWinSystem());
+ winSystem->OnAppFocusChange(false);
+
+ // Apple says to disable ZeroConfig when moving to background
+ //! @todo
+ //CNetworkServices::GetInstance().StopZeroconf();
+
+ if (m_isPlayingBeforeInactive)
+ {
+ // if we were playing and have paused, then
+ // enable a background task to keep the network alive
+ [self enableBackGroundTask];
+ }
+ else
+ {
+ // if we are not playing/pause when going to background
+ // close out network shares as we can get fully suspended.
+ g_application.CloseNetworkShares();
+ }
+
+ // OnAppFocusChange triggers an AE suspend.
+ // Wait for AE to suspend and delete the audio sink, this allows
+ // AudioOutputUnitStop to complete and AVAudioSession to be set inactive.
+ // Note that to user, we moved into background to user but we
+ // are really waiting here for AE to suspend.
+ //! @todo
+ /*
+ while (!CAEFactory::IsSuspended() && !timer.IsTimePast())
+ usleep(250*1000);
+ */
+}
+
+- (void)enterForeground
+{
+ // stop background task (if running)
+ [self disableBackGroundTask];
+
+ [NSThread detachNewThreadSelector:@selector(enterForegroundDelayed:)
+ toTarget:self
+ withObject:nil];
+}
+
+- (void)enterForegroundDelayed:(id)arg
+{
+ // MCRuntimeLib_Initialized is only true if
+ // we were running and got moved to background
+ while (!g_application.IsInitialized())
+ usleep(50 * 1000);
+
+ CWinSystemTVOS* winSystem = dynamic_cast<CWinSystemTVOS*>(CServiceBroker::GetWinSystem());
+ winSystem->OnAppFocusChange(true);
+
+ // when we come back, restore playing if we were.
+ if (m_isPlayingBeforeInactive)
+ {
+ CApplicationMessenger::GetInstance().SendMsg(TMSG_MEDIA_UNPAUSE);
+ m_isPlayingBeforeInactive = NO;
+ }
+ // restart ZeroConfig (if stopped)
+ //! @todo
+ //CNetworkServices::GetInstance().StartZeroconf();
+
+ // do not update if we are already updating
+ if (!(g_application.IsVideoScanning() || g_application.IsMusicScanning()))
+ g_application.UpdateLibraries();
+
+ // this will fire only if we are already alive and have 'menu'ed out and back
+ CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::System, "xbmc", "OnWake");
+
+ // this handles what to do if we got pushed
+ // into foreground by a topshelf item select/play
+ CTVOSTopShelf::GetInstance().RunTopShelf();
+}
+
+#pragma mark - ScreenSaver Idletimer
+
+- (void)disableScreenSaver
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
+ });
+}
+
+- (void)enableScreenSaver
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [[UIApplication sharedApplication] setIdleTimerDisabled:NO];
+ });
+}
+
+- (bool)resetSystemIdleTimer
+{
+ // this is silly :)
+ // when system screen saver kicks off, we switch to UIApplicationStateInactive, the only way
+ // to get out of the screensaver is to call ourself to open an custom URL that is registered
+ // in our Info.plist. The openURL method of UIApplication must be supported but we can just
+ // reply NO and we get restored to UIApplicationStateActive.
+ __block bool inActive = false;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ inActive = [UIApplication sharedApplication].applicationState == UIApplicationStateInactive;
+ if (inActive)
+ {
+ auto wakeupString =
+ [[NSArray arrayWithObjects:[NSString stringWithUTF8String:CCompileInfo::GetAppName()],
+ @"://wakeup", nil] componentsJoinedByString:@""];
+ NSURL* url = [NSURL URLWithString:wakeupString];
+ [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
+ }
+ });
+ return inActive;
+}
+
+#pragma mark - runtime routines
+
+- (void)pauseAnimation
+{
+ m_pause = YES;
+ g_application.SetRenderGUI(false);
+}
+
+- (void)resumeAnimation
+{
+ m_pause = NO;
+ g_application.SetRenderGUI(true);
+}
+
+- (void)startAnimation
+{
+ if (!m_animating && [glView getCurrentEAGLContext])
+ {
+ // kick off an animation thread
+ m_animationThreadLock = [[NSConditionLock alloc] initWithCondition:FALSE];
+ m_animationThread = [[NSThread alloc] initWithTarget:self
+ selector:@selector(runAnimation:)
+ object:m_animationThreadLock];
+ [m_animationThread start];
+ m_animating = YES;
+ }
+}
+
+- (void)stopAnimation
+{
+ if (!m_animating && [glView getCurrentEAGLContext])
+ {
+ m_appAlive = NO;
+ m_animating = NO;
+ if (!g_application.m_bStop)
+ {
+ CApplicationMessenger::GetInstance().PostMsg(TMSG_QUIT);
+ }
+
+ CAnnounceReceiver::GetInstance()->DeInitialize();
+
+ // wait for animation thread to die
+ if (!m_animationThread.finished)
+ [m_animationThreadLock lockWhenCondition:TRUE];
+ }
+}
+
+- (void)runAnimation:(id)arg
+{
+ @autoreleasepool
+ {
+ [NSThread currentThread].name = @"XBMC_Run";
+
+ // signal the thread is alive
+ NSConditionLock* myLock = arg;
+ [myLock lock];
+
+ // Prevent child processes from becoming zombies on exit
+ // if not waited upon. See also Util::Command
+ struct sigaction sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_flags = SA_NOCLDWAIT;
+ sa.sa_handler = SIG_IGN;
+ sigaction(SIGCHLD, &sa, NULL);
+
+ setlocale(LC_NUMERIC, "C");
+
+ int status = 0;
+ try
+ {
+ // set up some Kodi specific relationships
+ // XBMC::Context run_context; //! @todo
+ m_appAlive = YES;
+ // start up with gui enabled
+ status = KODI_Run(true);
+ // we exited or died.
+ g_application.SetRenderGUI(false);
+ }
+ catch (...)
+ {
+ m_appAlive = FALSE;
+ CLog::Log(LOGERROR, "%sException caught on main loop status=%d. Exiting", __PRETTY_FUNCTION__,
+ status);
+ }
+
+ // signal the thread is dead
+ [myLock unlockWithCondition:TRUE];
+
+ [self enableScreenSaver];
+ [self performSelectorOnMainThread:@selector(CallExit) withObject:nil waitUntilDone:NO];
+ }
+}
+
+#pragma mark - KODI_Run
+
+int KODI_Run(bool renderGUI)
+{
+ int status = -1;
+
+ CAppParamParser appParamParser; //! @todo : proper params
+ if (!g_application.Create(appParamParser))
+ {
+ CLog::Log(LOGERROR, "ERROR: Unable to create application. Exiting");
+ return status;
+ }
+
+ //this can't be set from CAdvancedSettings::Initialize()
+ //because it will overwrite the loglevel set with the --debug flag
+#ifdef _DEBUG
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel = LOG_LEVEL_DEBUG;
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevelHint = LOG_LEVEL_DEBUG;
+#else
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel = LOG_LEVEL_NORMAL;
+ CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevelHint = LOG_LEVEL_NORMAL;
+#endif
+ CLog::SetLogLevel(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_logLevel);
+
+ // not a failure if returns false, just means someone
+ // did the init before us.
+ if (!CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->Initialized())
+ {
+ //CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->Initialize();
+ //! @todo
+ }
+
+ CAnnounceReceiver::GetInstance()->Initialize();
+
+ if (renderGUI && !g_application.CreateGUI())
+ {
+ CLog::Log(LOGERROR, "ERROR: Unable to create GUI. Exiting");
+ return status;
+ }
+ if (!g_application.Initialize())
+ {
+ CLog::Log(LOGERROR, "ERROR: Unable to Initialize. Exiting");
+ return status;
+ }
+
+ try
+ {
+ status = g_application.Run(appParamParser);
+ }
+ catch (...)
+ {
+ CLog::Log(LOGERROR, "ERROR: Exception caught on main loop. Exiting");
+ status = -1;
+ }
+
+ return status;
+}
+
+- (void)CallExit
+{
+ exit(0);
+}
+
+- (AVDisplayManager*)avDisplayManager __attribute__((availability(tvos, introduced = 11.2)))
+{
+ return self.view.window.avDisplayManager;
+}
+
+#pragma mark - EAGLContext
+
+- (EAGLContext*)getEAGLContextObj
+{
+ return [glView getCurrentEAGLContext];
+}
+
+#pragma mark - remoteControlReceivedWithEvent forwarder
+// remoteControlReceived requires subclassing of UIViewController
+// Just implement as a forwarding class to CLibRemote so it doesnt need to subclass
+- (void)remoteControlReceivedWithEvent:(UIEvent*)receivedEvent
+{
+ if (receivedEvent.type == UIEventTypeRemoteControl)
+ {
+ [inputHandler.inputRemote remoteControlEvent:receivedEvent];
+ }
+}
+
+#pragma mark - init/deinit
+
+- (void)dealloc
+{
+ [displayManager removeModeSwitchObserver];
+ // stop background task (if running)
+ [self disableBackGroundTask];
+
+ [self stopAnimation];
+}
+
+- (instancetype)init
+{
+ self = [super init];
+ if (!self)
+ return nil;
+
+ m_pause = NO;
+ m_appAlive = NO;
+ m_animating = NO;
+
+ m_isPlayingBeforeInactive = NO;
+ m_bgTask = UIBackgroundTaskInvalid;
+
+ [self enableScreenSaver];
+
+ g_xbmcController = self;
+ MPNPInfoManager = [DarwinEmbedNowPlayingInfoManager new];
+ displayManager = [TVOSDisplayManager new];
+ inputHandler = [TVOSLibInputHandler new];
+
+ return self;
+}
+
+@end
+#undef BOOL
diff --git a/xbmc/platform/darwin/tvos/filesystem/CMakeLists.txt b/xbmc/platform/darwin/tvos/filesystem/CMakeLists.txt
new file mode 100644
index 0000000000..8722e75455
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/CMakeLists.txt
@@ -0,0 +1,9 @@
+set(SOURCES TVOSDirectory.cpp
+ TVOSFile.cpp
+ TVOSFileUtils.mm)
+
+set(HEADERS TVOSDirectory.h
+ TVOSFile.h
+ TVOSFileUtils.h)
+
+core_add_library(platform_tvos_filesystem)
diff --git a/xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.cpp b/xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.cpp
new file mode 100644
index 0000000000..2cc32ba050
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2018 Team MrMC
+ * https://github.com/MrMC
+ *
+ * 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 2, 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 MrMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "TVOSDirectory.h"
+
+#include "FileItem.h"
+#include "URL.h"
+#include "filesystem/SpecialProtocol.h"
+#include "utils/URIUtils.h"
+#include "utils/log.h"
+
+#include "platform/darwin/ios-common/DarwinNSUserDefaults.h"
+#include "platform/darwin/tvos/filesystem/TVOSFile.h"
+#include "platform/darwin/tvos/filesystem/TVOSFileUtils.h"
+#include "platform/posix/XTimeUtils.h"
+#include "platform/posix/filesystem/PosixDirectory.h"
+
+
+using namespace XFILE;
+
+bool CTVOSDirectory::WantsDirectory(const CURL& url)
+{
+ auto rootpath = CSpecialProtocol::TranslatePath(url);
+ auto found = rootpath.find(CTVOSFileUtils::GetUserHomeDirectory());
+ if (found == std::string::npos)
+ return false;
+
+ return true;
+}
+
+bool CTVOSDirectory::GetDirectory(const CURL& url, CFileItemList& items)
+{
+ // tvos keeps dirs and non-xml files in non-persistent Caches directory
+ // xml files are vectored into NSUserDefaults which is persistent, so
+ // we fetch dirs and non-xml files using CPosixDirectory, then fill
+ // in any missing xml files from NSUserDefaults. The incoming url
+ // will be a path, ending at some dir, so we only need to fetch any
+ // xml files that might be at that dir level.
+ bool rtn = CPosixDirectory::GetDirectory(url, items);
+ if (!rtn)
+ return false;
+
+ // To see user home xml files in the file manager,
+ // we have to populate a list on a directory request.
+ auto rootpath = CSpecialProtocol::TranslatePath(url);
+ // quick return check, we do not care if
+ // not going to user home.
+ auto found = rootpath.find(CTVOSFileUtils::GetUserHomeDirectory());
+ if (found == std::string::npos)
+ return rtn;
+
+ // The directory request will point to the right path '.../home/userdata/..'
+ // so we ask for files in ending directory, if we get xml file paths back
+ // then we are re-vectoring them to persistent storage and need to
+ // create CFileItems that will tranlate into CTVOSFile object later
+ // when accessed.
+
+ // GetDirectoryContents will return full paths
+ std::vector<std::string> contents;
+ CDarwinNSUserDefaults::GetDirectoryContents(rootpath, contents);
+ for (const auto& path : contents)
+ {
+ CFileItemPtr pItem(new CFileItem(URIUtils::GetFileName(path)));
+ // we only save files to persistent storage
+ pItem->m_bIsFolder = false;
+ // path must a full path, with no protocol
+ // or they will not get intercepted in CFileFactory
+ pItem->SetPath(path);
+ if (!(m_flags & DIR_FLAG_NO_FILE_INFO))
+ {
+ struct __stat64 buffer;
+ CTVOSFile tvOSFile;
+ CURL url2(pItem->GetPath());
+ if (tvOSFile.Stat(url2, &buffer) == 0)
+ {
+ // fake the datetime
+ FILETIME fileTime, localTime;
+ TimeTToFileTime(buffer.st_mtime, &fileTime);
+ FileTimeToLocalFileTime(&fileTime, &localTime);
+ pItem->m_dateTime = localTime;
+ // all this to get the file size
+ pItem->m_dwSize = buffer.st_size;
+ }
+ }
+ items.Add(pItem);
+ }
+
+ return rtn || !contents.empty();
+}
+
+bool CTVOSDirectory::Create(const CURL& url)
+{
+ return CPosixDirectory::Create(url);
+}
+
+bool CTVOSDirectory::Exists(const CURL& url)
+{
+ return CPosixDirectory::Exists(url);
+}
+
+bool CTVOSDirectory::Remove(const CURL& url)
+{
+ return CPosixDirectory::Remove(url);
+}
diff --git a/xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.h b/xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.h
new file mode 100644
index 0000000000..b57d42edcf
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/TVOSDirectory.h
@@ -0,0 +1,43 @@
+#pragma once
+/*
+ * Copyright (C) 2018 Team MrMC
+ * https://github.com/MrMC
+ *
+ * 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 2, 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 MrMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "filesystem/IDirectory.h"
+
+#include "platform/posix/filesystem/PosixDirectory.h"
+
+class CFileItemList;
+
+namespace XFILE
+{
+class CTVOSDirectory : public CPosixDirectory
+{
+public:
+ CTVOSDirectory() = default;
+ ~CTVOSDirectory() = default;
+
+ bool static WantsDirectory(const CURL& url);
+
+ bool GetDirectory(const CURL& url, CFileItemList& items) override;
+ bool Create(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ bool Remove(const CURL& url) override;
+};
+} // namespace XFILE
diff --git a/xbmc/platform/darwin/tvos/filesystem/TVOSFile.cpp b/xbmc/platform/darwin/tvos/filesystem/TVOSFile.cpp
new file mode 100644
index 0000000000..268300c8eb
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/TVOSFile.cpp
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2016 Team Kodi
+ * http://kodi.tv
+ *
+ * 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 2, 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 Kodi; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "TVOSFile.h"
+
+#include "filesystem/SpecialProtocol.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+
+#include "platform/darwin/ios-common/DarwinNSUserDefaults.h"
+#include "platform/posix/filesystem/PosixFile.h"
+
+#include <sys/stat.h>
+
+using namespace XFILE;
+
+CTVOSFile::~CTVOSFile()
+{
+ Close();
+}
+
+bool CTVOSFile::WantsFile(const CURL& url)
+{
+ if (!StringUtils::EqualsNoCase(url.GetFileType(), "xml") ||
+ StringUtils::StartsWithNoCase(url.GetFileNameWithoutPath(), "customcontroller.SiriRemote"))
+ return false;
+ return CDarwinNSUserDefaults::IsKeyFromPath(url.Get());
+}
+
+int CTVOSFile::CacheStat(const CURL& url, struct __stat64* buffer)
+{
+ if (buffer != nullptr)
+ {
+ size_t size = 0;
+ // get the size from the data by passing in a nullptr
+ if (CDarwinNSUserDefaults::GetKeyDataFromPath(url.Get(), nullptr, size))
+ {
+ memset(buffer, 0, sizeof(struct __stat64));
+ // mimic stat
+ // rw for world
+ // regular file
+ buffer->st_flags = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IWGRP | S_IWOTH | S_IFREG;
+ buffer->st_size = size;
+ buffer->st_blocks = 1; // we mimic one block
+ buffer->st_blksize = (blksize_t)size; // with full size
+ return 0;
+ }
+ }
+ errno = ENOENT;
+ return -1;
+}
+
+bool CTVOSFile::Open(const CURL& url)
+{
+ if (CDarwinNSUserDefaults::KeyFromPathExists(url.Get()))
+ {
+ m_url = url;
+ m_position = 0;
+ CacheStat(url, &m_cachedStat);
+ return true;
+ }
+ else
+ {
+ // fallback to posixfile
+ m_pFallbackFile = new CPosixFile();
+ return m_pFallbackFile->Open(url);
+ }
+}
+
+bool CTVOSFile::OpenForWrite(const CURL& url, bool bOverWrite /* = false */)
+{
+ if (CDarwinNSUserDefaults::KeyFromPathExists(url.Get()) && !bOverWrite)
+ return false; // no overwrite
+
+ bool ret = WantsFile(url); // if we want the file we can write it ...
+ if (ret)
+ {
+ m_url = url;
+ m_position = 0;
+ }
+ return ret;
+}
+
+bool CTVOSFile::Delete(const CURL& url)
+{
+ bool ret = CDarwinNSUserDefaults::DeleteKeyFromPath(url.Get(), true);
+
+ if (!ret)
+ {
+ CPosixFile posix;
+ ret = posix.Delete(url);
+ }
+ return ret;
+}
+
+bool CTVOSFile::Exists(const CURL& url)
+{
+ bool ret = CDarwinNSUserDefaults::KeyFromPathExists(url.Get());
+ if (!ret)
+ {
+ CPosixFile posix;
+ ret = posix.Exists(url);
+ }
+ return ret;
+}
+
+int CTVOSFile::Stat(const CURL& url, struct __stat64* buffer)
+{
+ int ret = CacheStat(url, buffer);
+ if (ret < 0)
+ {
+ CPosixFile posix;
+ ret = posix.Stat(url, buffer);
+ }
+ return ret;
+}
+
+bool CTVOSFile::Rename(const CURL& url, const CURL& urlnew)
+{
+ bool ret = false;
+ if (Exists(url) && !Exists(urlnew) && WantsFile(urlnew))
+ {
+ void* lpBuf = nullptr;
+ size_t uiBufSize = 0;
+ if (CDarwinNSUserDefaults::GetKeyDataFromPath(url.Get(), lpBuf,
+ uiBufSize)) // get size from old file
+ {
+ lpBuf = static_cast<void*>(new char[uiBufSize]);
+ if (CDarwinNSUserDefaults::GetKeyDataFromPath(url.Get(), lpBuf, uiBufSize)) // read old file
+ {
+ if (CDarwinNSUserDefaults::SetKeyDataFromPath(urlnew.Get(), lpBuf, uiBufSize,
+ true)) // write to new url
+ {
+ // remove old file
+ Delete(url);
+ ret = true;
+ }
+ }
+ delete[] static_cast<char*>(lpBuf);
+ }
+ }
+
+ if (!ret)
+ {
+ CPosixFile posix;
+ ret = posix.Rename(url, urlnew);
+ }
+ return ret;
+}
+
+int CTVOSFile::Stat(struct __stat64* buffer)
+{
+ memcpy(buffer, &m_cachedStat, sizeof(struct __stat64));
+ return 0;
+}
+
+ssize_t CTVOSFile::Read(void* lpBuf, size_t uiBufSize)
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->Read(lpBuf, uiBufSize);
+
+ void* lpBufInternal = nullptr;
+
+ if (m_position > 0 && m_position == GetLength())
+ return 0; // simulate read 0 bytes on EOF
+
+ if (CDarwinNSUserDefaults::GetKeyDataFromPath(m_url.Get(), lpBufInternal,
+ uiBufSize)) // read size of file
+ {
+ lpBufInternal = static_cast<void*>(new char[uiBufSize]);
+ if (CDarwinNSUserDefaults::GetKeyDataFromPath(m_url.Get(), lpBufInternal,
+ uiBufSize)) // read file
+ {
+ memcpy(lpBuf, lpBufInternal, uiBufSize);
+ }
+ delete[] static_cast<char*>(lpBufInternal);
+ m_position = uiBufSize;
+ }
+ return uiBufSize;
+}
+
+ssize_t CTVOSFile::Write(const void* lpBuf, size_t uiBufSize)
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->Write(lpBuf, uiBufSize);
+
+ if (CDarwinNSUserDefaults::SetKeyDataFromPath(m_url.Get(), lpBuf, uiBufSize,
+ true)) // write to file
+ {
+ m_position = uiBufSize;
+ CacheStat(m_url, &m_cachedStat);
+ return uiBufSize;
+ }
+ return -1;
+}
+
+int64_t CTVOSFile::Seek(int64_t iFilePosition, int iWhence /*=SEEK_SET*/)
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->Seek(iFilePosition, iWhence);
+
+ errno = EINVAL;
+ return -1;
+}
+
+void CTVOSFile::Close()
+{
+ m_url.Reset();
+ m_position = -1;
+ memset(&m_cachedStat, 0, sizeof(m_cachedStat));
+ if (m_pFallbackFile != nullptr)
+ {
+ m_pFallbackFile->Close();
+ delete m_pFallbackFile;
+ m_pFallbackFile = nullptr;
+ }
+}
+
+int64_t CTVOSFile::GetPosition()
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->GetPosition();
+
+ return 0;
+}
+
+int64_t CTVOSFile::GetLength()
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->GetLength();
+ else
+ return m_cachedStat.st_size;
+}
+
+int CTVOSFile::GetChunkSize()
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->GetChunkSize();
+ else
+ return static_cast<int>(GetLength()); // only full file size can be read from nsuserdefaults...
+}
+
+int CTVOSFile::IoControl(EIoControl request, void* param)
+{
+ if (m_pFallbackFile != nullptr)
+ return m_pFallbackFile->IoControl(request, param);
+
+ if (request == IOCTRL_SEEK_POSSIBLE)
+ return 0; // no seek support
+ return -1;
+}
diff --git a/xbmc/platform/darwin/tvos/filesystem/TVOSFile.h b/xbmc/platform/darwin/tvos/filesystem/TVOSFile.h
new file mode 100644
index 0000000000..483e7a1ee9
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/TVOSFile.h
@@ -0,0 +1,62 @@
+#pragma once
+/*
+ * Copyright (C) 2016 Team Kodi
+ * http://kodi.tv
+ *
+ * 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 2, 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 Kodi; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "URL.h"
+#include "filesystem/IFile.h"
+
+#include "platform/posix/filesystem/PosixFile.h"
+
+namespace XFILE
+{
+class CTVOSFile : public IFile
+{
+public:
+ CTVOSFile() : m_position(-1), m_pFallbackFile(nullptr){};
+ ~CTVOSFile();
+
+ bool static WantsFile(const CURL& url);
+
+ bool Open(const CURL& url) override;
+ bool Exists(const CURL& url) override;
+ int Stat(const CURL& url, struct __stat64* buffer) override;
+ int Stat(struct __stat64* buffer) override;
+ bool OpenForWrite(const CURL& url, bool bOverWrite = false) override;
+ bool Delete(const CURL& url) override;
+ bool Rename(const CURL& url, const CURL& urlnew) override;
+
+ ssize_t Read(void* lpBuf, size_t uiBufSize) override;
+ ssize_t Write(const void* lpBuf, size_t uiBufSize) override;
+ int64_t Seek(int64_t iFilePosition, int iWhence = SEEK_SET) override;
+ void Close() override;
+ int64_t GetPosition() override;
+ int64_t GetLength() override;
+ int GetChunkSize() override;
+ int IoControl(EIoControl request, void* param) override;
+
+protected:
+ CURL m_url;
+ int64_t m_position;
+ CPosixFile* m_pFallbackFile;
+ struct __stat64 m_cachedStat;
+
+ int CacheStat(const CURL& url, struct __stat64* buffer);
+};
+} // namespace XFILE
diff --git a/xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.h b/xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.h
new file mode 100644
index 0000000000..23b2cd31d1
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+class CTVOSFileUtils
+{
+public:
+ static const char* GetUserHomeDirectory();
+ static const char* GetOSCachesDirectory();
+};
diff --git a/xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.mm b/xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.mm
new file mode 100644
index 0000000000..8877c99e44
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/filesystem/TVOSFileUtils.mm
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "TVOSFileUtils.h"
+
+#import "CompileInfo.h"
+#import "utils/URIUtils.h"
+
+#import <mutex>
+
+#import <Foundation/Foundation.h>
+
+const char* CTVOSFileUtils::GetUserHomeDirectory()
+{
+ static std::string appHomeFolder;
+ static std::once_flag dir_flag;
+
+ std::call_once(dir_flag, [] {
+ appHomeFolder = URIUtils::AddFileToFolder(GetOSCachesDirectory(), CCompileInfo::GetAppName());
+ });
+
+ return appHomeFolder.c_str();
+}
+
+const char* CTVOSFileUtils::GetOSCachesDirectory()
+{
+ static std::string cacheFolder;
+ static std::once_flag cache_flag;
+
+ std::call_once(cache_flag, [] {
+ NSString* cachePath =
+ NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
+ cacheFolder = cachePath.UTF8String;
+ URIUtils::RemoveSlashAtEnd(cacheFolder);
+ });
+ return cacheFolder.c_str();
+}
diff --git a/xbmc/platform/darwin/tvos/input/CMakeLists.txt b/xbmc/platform/darwin/tvos/input/CMakeLists.txt
new file mode 100644
index 0000000000..67eaecf868
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/CMakeLists.txt
@@ -0,0 +1,11 @@
+set(SOURCES LibInputHandler.mm
+ LibInputRemote.mm
+ LibInputSettings.mm
+ LibInputTouch.mm)
+
+set(HEADERS LibInputHandler.h
+ LibInputRemote.h
+ LibInputSettings.h
+ LibInputTouch.h)
+
+core_add_library(platform_tvos_input)
diff --git a/xbmc/platform/darwin/tvos/input/LibInputHandler.h b/xbmc/platform/darwin/tvos/input/LibInputHandler.h
new file mode 100644
index 0000000000..db194312c3
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputHandler.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class TVOSLibInputRemote;
+@class TVOSLibInputSettings;
+@class TVOSLibInputTouch;
+
+@interface TVOSLibInputHandler : NSObject
+
+@property(nonatomic, strong) TVOSLibInputRemote* inputRemote;
+@property(nonatomic, strong) TVOSLibInputSettings* inputSettings;
+@property(nonatomic, strong) TVOSLibInputTouch* inputTouch;
+
+- (void)sendButtonPressed:(int)buttonId;
+- (instancetype)init;
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputHandler.mm b/xbmc/platform/darwin/tvos/input/LibInputHandler.mm
new file mode 100644
index 0000000000..671c994d2b
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputHandler.mm
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "LibInputHandler.h"
+
+#include "Application.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/CustomControllerTranslator.h"
+#include "input/InputManager.h"
+#include "utils/log.h"
+
+#import "platform/darwin/tvos/input/LibInputRemote.h"
+#import "platform/darwin/tvos/input/LibInputSettings.h"
+#import "platform/darwin/tvos/input/LibInputTouch.h"
+
+@implementation TVOSLibInputHandler
+
+@synthesize inputRemote;
+@synthesize inputSettings;
+@synthesize inputTouch;
+
+#pragma mark - internal key press methods
+
+//! @Todo: factor out siriremote customcontroller to a setting?
+// allow to select multiple customcontrollers via setting list?
+- (void)sendButtonPressed:(int)buttonId
+{
+ int actionID;
+ std::string actionName;
+
+ // Translate using custom controller translator.
+ if (CServiceBroker::GetInputManager().TranslateCustomControllerString(
+ CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog(), "SiriRemote",
+ buttonId, actionID, actionName))
+ {
+ // break screensaver
+ g_application.ResetSystemIdleTimer();
+ g_application.ResetScreenSaver();
+
+ // in case we wokeup the screensaver or screen - eat that action...
+ if (g_application.WakeUpScreenSaverAndDPMS())
+ return;
+ CServiceBroker::GetInputManager().QueueAction(CAction(actionID, 1.0f, 0.0f, actionName));
+ }
+ else
+ {
+ CLog::Log(LOGDEBUG, "ERROR mapping customcontroller action. CustomController: %s %i",
+ "SiriRemote", buttonId);
+ }
+}
+
+- (instancetype)init
+{
+ self = [super init];
+ if (!self)
+ return nil;
+
+ inputRemote = [TVOSLibInputRemote new];
+ inputSettings = [TVOSLibInputSettings new];
+ inputTouch = [TVOSLibInputTouch new];
+
+ return self;
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputRemote.h b/xbmc/platform/darwin/tvos/input/LibInputRemote.h
new file mode 100644
index 0000000000..d83e0be1c3
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputRemote.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <Foundation/NSTimer.h>
+#import <UIKit/UIEvent.h>
+
+@interface TVOSLibInputRemote : NSObject
+{
+ NSTimer* m_pressAutoRepeatTimer;
+ NSTimer* m_remoteIdleTimer;
+}
+
+@property(nonatomic) BOOL remoteIdleState;
+
+- (void)startRemoteTimer;
+- (void)stopRemoteTimer;
+- (void)setRemoteIdleState;
+- (void)startKeyPressTimer:(int)keyId;
+- (void)startKeyPressTimer:(int)keyId clickTime:(NSTimeInterval)interval;
+- (void)stopKeyPressTimer;
+- (void)keyPressTimerCallback:(NSTimer*)theTimer;
+- (void)remoteControlEvent:(UIEvent*)receivedEvent;
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputRemote.mm b/xbmc/platform/darwin/tvos/input/LibInputRemote.mm
new file mode 100644
index 0000000000..452ddcbce3
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputRemote.mm
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "LibInputRemote.h"
+
+#include "Application.h"
+#include "ServiceBroker.h"
+#include "input/actions/Action.h"
+#include "messaging/ApplicationMessenger.h"
+#import "windowing/tvos/WinSystemTVOS.h"
+
+#import "platform/darwin/tvos/XBMCController.h"
+#import "platform/darwin/tvos/input/LibInputHandler.h"
+#import "platform/darwin/tvos/input/LibInputSettings.h"
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIEvent.h>
+
+@implementation TVOSLibInputRemote
+
+@synthesize remoteIdleState = m_remoteIdleState;
+
+// Default Timer values (seconds)
+NSTimeInterval REPEATED_KEYPRESS_DELAY_S = 0.50;
+NSTimeInterval REPEATED_KEYPRESS_PAUSE_S = 0.05;
+
+#pragma mark - remote idle timer
+
+- (void)startRemoteTimer
+{
+ m_remoteIdleState = false;
+
+ if (m_remoteIdleTimer != nil)
+ [self stopRemoteTimer];
+ if (g_xbmcController.inputHandler.inputSettings.remoteIdleEnabled)
+ {
+ auto fireDate = [NSDate
+ dateWithTimeIntervalSinceNow:g_xbmcController.inputHandler.inputSettings.remoteIdleTimeout];
+ auto timer = [[NSTimer alloc] initWithFireDate:fireDate
+ interval:0.0
+ target:self
+ selector:@selector(setRemoteIdleState)
+ userInfo:nil
+ repeats:NO];
+
+ [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
+ m_remoteIdleTimer = timer;
+ }
+}
+
+- (void)stopRemoteTimer
+{
+ [m_remoteIdleTimer invalidate];
+ m_remoteIdleTimer = nil;
+ m_remoteIdleState = NO;
+}
+
+- (void)setRemoteIdleState
+{
+ m_remoteIdleState = YES;
+}
+
+#pragma mark - key press auto-repeat methods
+
+- (void)startKeyPressTimer:(int)keyId
+{
+ [self startKeyPressTimer:keyId clickTime:REPEATED_KEYPRESS_PAUSE_S];
+}
+
+- (void)startKeyPressTimer:(int)keyId clickTime:(NSTimeInterval)interval
+{
+ if (m_pressAutoRepeatTimer != nil)
+ [self stopKeyPressTimer];
+
+ [g_xbmcController.inputHandler sendButtonPressed:keyId];
+
+ NSNumber* number = @(keyId);
+ auto fireDate = [NSDate dateWithTimeIntervalSinceNow:REPEATED_KEYPRESS_DELAY_S];
+
+ // schedule repeated timer which starts after REPEATED_KEYPRESS_DELAY_S
+ // and fires every REPEATED_KEYPRESS_PAUSE_S
+ auto timer = [[NSTimer alloc] initWithFireDate:fireDate
+ interval:interval
+ target:self
+ selector:@selector(keyPressTimerCallback:)
+ userInfo:number
+ repeats:YES];
+
+ // schedule the timer to the runloop
+ [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
+ m_pressAutoRepeatTimer = timer;
+}
+
+- (void)stopKeyPressTimer
+{
+ [m_pressAutoRepeatTimer invalidate];
+ m_pressAutoRepeatTimer = nil;
+}
+
+- (void)keyPressTimerCallback:(NSTimer*)theTimer
+{
+ // if queue is empty - skip this timer event before letting it process
+ CWinSystemTVOS* winSystem(dynamic_cast<CWinSystemTVOS*>(CServiceBroker::GetWinSystem()));
+ if (!winSystem->GetQueueSize())
+ [g_xbmcController.inputHandler sendButtonPressed:[theTimer.userInfo intValue]];
+}
+
+#pragma mark - remoteControlEventwith
+
+- (void)remoteControlEvent:(UIEvent*)receivedEvent
+{
+ switch (receivedEvent.subtype)
+ {
+ case UIEventSubtypeRemoteControlTogglePlayPause:
+ KODI::MESSAGING::CApplicationMessenger::GetInstance().PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_PLAYER_PLAYPAUSE)));
+ break;
+ case UIEventSubtypeRemoteControlPlay:
+ [g_xbmcController.inputHandler sendButtonPressed:13];
+ break;
+ case UIEventSubtypeRemoteControlPause:
+ [g_xbmcController.inputHandler sendButtonPressed:14];
+ break;
+ case UIEventSubtypeRemoteControlStop:
+ [g_xbmcController.inputHandler sendButtonPressed:15];
+ break;
+ case UIEventSubtypeRemoteControlNextTrack:
+ [g_xbmcController.inputHandler sendButtonPressed:16];
+ break;
+ case UIEventSubtypeRemoteControlPreviousTrack:
+ [g_xbmcController.inputHandler sendButtonPressed:17];
+ break;
+ case UIEventSubtypeRemoteControlBeginSeekingForward:
+ [g_xbmcController.inputHandler sendButtonPressed:18];
+ break;
+ case UIEventSubtypeRemoteControlBeginSeekingBackward:
+ [g_xbmcController.inputHandler sendButtonPressed:19];
+ break;
+ case UIEventSubtypeRemoteControlEndSeekingForward:
+ case UIEventSubtypeRemoteControlEndSeekingBackward:
+ // restore to normal playback speed.
+ if (g_application.GetAppPlayer().IsPlaying() && !g_application.GetAppPlayer().IsPaused())
+ KODI::MESSAGING::CApplicationMessenger::GetInstance().PostMsg(
+ TMSG_GUI_ACTION, WINDOW_INVALID, -1,
+ static_cast<void*>(new CAction(ACTION_PLAYER_PLAY)));
+ break;
+ default:
+ break;
+ }
+ // start remote timeout
+ [self startRemoteTimer];
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputSettings.h b/xbmc/platform/darwin/tvos/input/LibInputSettings.h
new file mode 100644
index 0000000000..70606a32d1
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputSettings.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface TVOSLibInputSettings : NSObject
+
+@property(nonatomic) bool useSiriRemote;
+@property(nonatomic) BOOL remoteIdleEnabled;
+@property(nonatomic) int remoteIdleTimeout;
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputSettings.mm b/xbmc/platform/darwin/tvos/input/LibInputSettings.mm
new file mode 100644
index 0000000000..183be43b8d
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputSettings.mm
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "LibInputSettings.h"
+
+#import "platform/darwin/tvos/XBMCController.h"
+#import "platform/darwin/tvos/input/LibInputHandler.h"
+#import "platform/darwin/tvos/input/LibInputRemote.h"
+
+#import <Foundation/Foundation.h>
+
+@implementation TVOSLibInputSettings
+
+@synthesize useSiriRemote = m_useSiriRemote;
+@synthesize remoteIdleEnabled = m_remoteIdleEnabled;
+@synthesize remoteIdleTimeout = m_remoteIdleTimeout;
+
+- (void)setRemoteIdleEnabled:(BOOL)idle
+{
+ if (m_remoteIdleEnabled != idle)
+ {
+ m_remoteIdleEnabled = idle;
+ if (m_remoteIdleEnabled == YES)
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ }
+}
+
+- (void)setRemoteIdleTimeout:(int)timeout
+{
+ m_remoteIdleTimeout = timeout;
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputTouch.h b/xbmc/platform/darwin/tvos/input/LibInputTouch.h
new file mode 100644
index 0000000000..d7f86a0f0b
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputTouch.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <UIKit/UIKit.h>
+
+typedef NS_ENUM(NSUInteger, UIPanGestureRecognizerDirection) {
+ UIPanGestureRecognizerDirectionUndefined,
+ UIPanGestureRecognizerDirectionUp,
+ UIPanGestureRecognizerDirectionDown,
+ UIPanGestureRecognizerDirectionLeft,
+ UIPanGestureRecognizerDirectionRight
+};
+
+@interface TVOSLibInputTouch : NSObject <UIGestureRecognizerDelegate>
+{
+ UIPanGestureRecognizerDirection m_direction;
+ BOOL m_directionOverride;
+ CGPoint m_lastGesturePoint;
+ unsigned long m_touchDirection;
+ BOOL m_touchBeginSignaled;
+}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer;
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldReceivePress:(UIPress*)press;
+- (void)createSwipeGestureRecognizers;
+- (void)createPanGestureRecognizers;
+- (void)createTapGesturecognizers;
+- (void)createPressGesturecognizers;
+- (void)menuPressed:(UITapGestureRecognizer*)sender;
+- (void)SiriLongSelectHandler:(UIGestureRecognizer*)sender;
+- (void)SiriSelectHandler:(UITapGestureRecognizer*)sender;
+- (void)playPausePressed:(UITapGestureRecognizer*)sender;
+- (void)longPlayPausePressed:(UILongPressGestureRecognizer*)sender;
+- (void)doublePlayPausePressed:(UITapGestureRecognizer*)sender;
+- (void)SiriDoubleSelectHandler:(UITapGestureRecognizer*)sender;
+- (IBAction)IRRemoteUpArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)IRRemoteDownArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)IRRemoteLeftArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)IRRemoteRightArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)tapUpArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)tapDownArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)tapLeftArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)tapRightArrowPressed:(UIGestureRecognizer*)sender;
+- (IBAction)handlePan:(UIPanGestureRecognizer*)sender;
+- (IBAction)handleSwipe:(UISwipeGestureRecognizer*)sender;
+- (UIPanGestureRecognizerDirection)getPanDirection:(CGPoint)translation;
+- (BOOL)shouldFastScroll;
+
+@end
diff --git a/xbmc/platform/darwin/tvos/input/LibInputTouch.mm b/xbmc/platform/darwin/tvos/input/LibInputTouch.mm
new file mode 100644
index 0000000000..8db994eace
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/input/LibInputTouch.mm
@@ -0,0 +1,646 @@
+/*
+ * Copyright (C) 2019- Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "LibInputTouch.h"
+
+#include "Application.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "utils/log.h"
+
+#import "platform/darwin/tvos/TVOSEAGLView.h"
+#import "platform/darwin/tvos/XBMCController.h"
+#import "platform/darwin/tvos/input/LibInputHandler.h"
+#import "platform/darwin/tvos/input/LibInputRemote.h"
+#import "platform/darwin/tvos/input/LibInputSettings.h"
+
+#include <tuple>
+
+#import <UIKit/UIKit.h>
+
+@class XBMCController;
+
+@implementation TVOSLibInputTouch
+
+#pragma mark - gesture methods
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
+ shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
+{
+ if ([gestureRecognizer isKindOfClass:[UISwipeGestureRecognizer class]] &&
+ [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
+ {
+ return YES;
+ }
+ if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
+ [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
+ {
+ return YES;
+ }
+ if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] &&
+ [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
+ {
+ return YES;
+ }
+ return NO;
+}
+
+// called before pressesBegan:withEvent: is called on the gesture recognizer
+// for a new press. return NO to prevent the gesture recognizer from seeing this press
+- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer shouldReceivePress:(UIPress*)press
+{
+ BOOL handled = YES;
+ switch (press.type)
+ {
+ // single press key, but also detect hold and back to tvos.
+ case UIPressTypeMenu:
+ // menu is special.
+ // a) if at our home view, should return to atv home screen.
+ // b) if not, let it pass to us.
+ if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_HOME &&
+ !CServiceBroker::GetGUI()->GetWindowManager().HasVisibleModalDialog() &&
+ !g_application.GetAppPlayer().IsPlaying())
+ handled = NO;
+ break;
+
+ // single press keys
+ case UIPressTypeSelect:
+ case UIPressTypePlayPause:
+ break;
+
+ // auto-repeat keys
+ case UIPressTypeUpArrow:
+ case UIPressTypeDownArrow:
+ case UIPressTypeLeftArrow:
+ case UIPressTypeRightArrow:
+ break;
+
+ default:
+ handled = NO;
+ }
+
+ return handled;
+}
+
+- (void)createSwipeGestureRecognizers
+{
+ for (auto swipeDirection :
+ {UISwipeGestureRecognizerDirectionLeft, UISwipeGestureRecognizerDirectionRight,
+ UISwipeGestureRecognizerDirectionUp, UISwipeGestureRecognizerDirectionDown})
+ {
+ auto swipeRecognizer =
+ [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
+ swipeRecognizer.delaysTouchesBegan = NO;
+ swipeRecognizer.direction = swipeDirection;
+ swipeRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:swipeRecognizer];
+ }
+}
+
+- (void)createPanGestureRecognizers
+{
+ // for pan gestures with one finger
+ auto pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
+ pan.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:pan];
+}
+
+- (void)createTapGesturecognizers
+{
+ // tap side of siri remote pad
+ for (auto t : {
+ std::make_tuple(UIPressTypeUpArrow, @selector(tapUpArrowPressed:),
+ @selector(IRRemoteUpArrowPressed:)),
+ std::make_tuple(UIPressTypeDownArrow, @selector(tapDownArrowPressed:),
+ @selector(IRRemoteDownArrowPressed:)),
+ std::make_tuple(UIPressTypeLeftArrow, @selector(tapLeftArrowPressed:),
+ @selector(IRRemoteLeftArrowPressed:)),
+ std::make_tuple(UIPressTypeRightArrow, @selector(tapRightArrowPressed:),
+ @selector(IRRemoteRightArrowPressed:))
+ })
+ {
+ auto allowedPressTypes = @[ @(std::get<0>(t)) ];
+
+ auto arrowRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self action:std::get<1>(t)];
+ arrowRecognizer.allowedPressTypes = allowedPressTypes;
+ arrowRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:arrowRecognizer];
+
+ // @todo doesn't seem to work
+ // we need UILongPressGestureRecognizer here because it will give
+ // UIGestureRecognizerStateBegan AND UIGestureRecognizerStateEnded
+ // even if we hold down for a long time. UITapGestureRecognizer
+ // will eat the ending on long holds and we never see it.
+ auto longArrowRecognizer =
+ [[UILongPressGestureRecognizer alloc] initWithTarget:self action:std::get<2>(t)];
+ longArrowRecognizer.allowedPressTypes = allowedPressTypes;
+ longArrowRecognizer.minimumPressDuration = 0.01;
+ longArrowRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:longArrowRecognizer];
+ }
+}
+
+- (void)createPressGesturecognizers
+{
+ auto menuRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(menuPressed:)];
+ menuRecognizer.allowedPressTypes = @[ @(UIPressTypeMenu) ];
+ menuRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:menuRecognizer];
+
+ auto playPauseTypes = @[ @(UIPressTypePlayPause) ];
+ auto playPauseRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(playPausePressed:)];
+ playPauseRecognizer.allowedPressTypes = playPauseTypes;
+ playPauseRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:playPauseRecognizer];
+
+ auto doublePlayPauseRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(doublePlayPausePressed:)];
+ doublePlayPauseRecognizer.allowedPressTypes = playPauseTypes;
+ doublePlayPauseRecognizer.numberOfTapsRequired = 2;
+ doublePlayPauseRecognizer.delegate = self;
+ [g_xbmcController.glView.gestureRecognizers.lastObject
+ requireGestureRecognizerToFail:doublePlayPauseRecognizer];
+ [g_xbmcController.glView addGestureRecognizer:doublePlayPauseRecognizer];
+
+ auto longPlayPauseRecognizer =
+ [[UILongPressGestureRecognizer alloc] initWithTarget:self
+ action:@selector(longPlayPausePressed:)];
+ longPlayPauseRecognizer.allowedPressTypes = playPauseTypes;
+ longPlayPauseRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:longPlayPauseRecognizer];
+
+ auto selectTypes = @[ @(UIPressTypeSelect) ];
+ auto longSelectRecognizer =
+ [[UILongPressGestureRecognizer alloc] initWithTarget:self
+ action:@selector(SiriLongSelectHandler:)];
+ longSelectRecognizer.allowedPressTypes = selectTypes;
+ longSelectRecognizer.minimumPressDuration = 0.001;
+ longSelectRecognizer.delegate = self;
+ [g_xbmcController.glView addGestureRecognizer:longSelectRecognizer];
+
+ auto selectRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(SiriSelectHandler:)];
+ selectRecognizer.allowedPressTypes = selectTypes;
+ selectRecognizer.delegate = self;
+ [longSelectRecognizer requireGestureRecognizerToFail:selectRecognizer];
+ [g_xbmcController.glView addGestureRecognizer:selectRecognizer];
+
+ auto doubleSelectRecognizer =
+ [[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(SiriDoubleSelectHandler:)];
+ doubleSelectRecognizer.allowedPressTypes = selectTypes;
+ doubleSelectRecognizer.numberOfTapsRequired = 2;
+ doubleSelectRecognizer.delegate = self;
+ [longSelectRecognizer requireGestureRecognizerToFail:doubleSelectRecognizer];
+ [g_xbmcController.glView.gestureRecognizers.lastObject
+ requireGestureRecognizerToFail:doubleSelectRecognizer];
+ [g_xbmcController.glView addGestureRecognizer:doubleSelectRecognizer];
+}
+
+- (void)menuPressed:(UITapGestureRecognizer*)sender
+{
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ break;
+ case UIGestureRecognizerStateChanged:
+ break;
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler sendButtonPressed:6];
+
+ // start remote timeout
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ default:
+ break;
+ }
+}
+
+- (void)SiriLongSelectHandler:(UIGestureRecognizer*)sender
+{
+ if (sender.state == UIGestureRecognizerStateBegan)
+ {
+ [g_xbmcController.inputHandler sendButtonPressed:7];
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ }
+}
+
+- (void)SiriSelectHandler:(UITapGestureRecognizer*)sender
+{
+ CLog::Log(LOGDEBUG, "SiriSelectHandler");
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler sendButtonPressed:5];
+ break;
+ default:
+ break;
+ }
+}
+
+- (void)playPausePressed:(UITapGestureRecognizer*)sender
+{
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ break;
+ case UIGestureRecognizerStateChanged:
+ break;
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler sendButtonPressed:12];
+ // start remote timeout
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ default:
+ break;
+ }
+}
+
+- (void)longPlayPausePressed:(UILongPressGestureRecognizer*)sender
+{
+ CLog::Log(LOGDEBUG, "Input: play/pause long press, state: %ld", static_cast<long>(sender.state));
+}
+
+- (void)doublePlayPausePressed:(UITapGestureRecognizer*)sender
+{
+ // state is only UIGestureRecognizerStateBegan and UIGestureRecognizerStateEnded
+ CLog::Log(LOGDEBUG, "Input: play/pause double press");
+}
+
+- (void)SiriDoubleSelectHandler:(UITapGestureRecognizer*)sender
+{
+ CLog::Log(LOGDEBUG, "Input: select double press");
+}
+
+#pragma mark - IR Arrows Pressed
+
+- (IBAction)IRRemoteUpArrowPressed:(UIGestureRecognizer*)sender
+{
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ [g_xbmcController.inputHandler.inputRemote startKeyPressTimer:1];
+ break;
+ case UIGestureRecognizerStateChanged:
+ break;
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler.inputRemote stopKeyPressTimer];
+ // start remote timeout
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ default:
+ break;
+ }
+}
+
+- (IBAction)IRRemoteDownArrowPressed:(UIGestureRecognizer*)sender
+{
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ [g_xbmcController.inputHandler.inputRemote startKeyPressTimer:2];
+ break;
+ case UIGestureRecognizerStateChanged:
+ break;
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler.inputRemote stopKeyPressTimer];
+ // start remote timeout
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ default:
+ break;
+ }
+}
+
+- (IBAction)IRRemoteLeftArrowPressed:(UIGestureRecognizer*)sender
+{
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ [g_xbmcController.inputHandler.inputRemote startKeyPressTimer:3];
+ break;
+ case UIGestureRecognizerStateChanged:
+ break;
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler.inputRemote stopKeyPressTimer];
+ // start remote timeout
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ default:
+ break;
+ }
+}
+
+- (IBAction)IRRemoteRightArrowPressed:(UIGestureRecognizer*)sender
+{
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ [g_xbmcController.inputHandler.inputRemote startKeyPressTimer:4];
+ break;
+ case UIGestureRecognizerStateChanged:
+ break;
+ case UIGestureRecognizerStateEnded:
+ [g_xbmcController.inputHandler.inputRemote stopKeyPressTimer];
+ // start remote timeout
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ default:
+ break;
+ }
+}
+
+#pragma mark - Tap Arrows
+
+- (IBAction)tapUpArrowPressed:(UIGestureRecognizer*)sender
+{
+ if (!g_xbmcController.inputHandler.inputRemote.remoteIdleState)
+ [g_xbmcController.inputHandler sendButtonPressed:1];
+
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+}
+
+- (IBAction)tapDownArrowPressed:(UIGestureRecognizer*)sender
+{
+ if (!g_xbmcController.inputHandler.inputRemote.remoteIdleState)
+ [g_xbmcController.inputHandler sendButtonPressed:2];
+
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+}
+
+- (IBAction)tapLeftArrowPressed:(UIGestureRecognizer*)sender
+{
+ if (!g_xbmcController.inputHandler.inputRemote.remoteIdleState)
+ [g_xbmcController.inputHandler sendButtonPressed:3];
+
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+}
+
+- (IBAction)tapRightArrowPressed:(UIGestureRecognizer*)sender
+{
+ if (!g_xbmcController.inputHandler.inputRemote.remoteIdleState)
+ [g_xbmcController.inputHandler sendButtonPressed:4];
+
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+}
+
+#pragma mark - Pan
+
+- (IBAction)handlePan:(UIPanGestureRecognizer*)sender
+{
+ if (g_xbmcController.inputHandler.inputRemote.remoteIdleState)
+ return;
+
+ if (!g_xbmcController.appAlive) //NO GESTURES BEFORE WE ARE UP AND RUNNING
+ return;
+
+ if ([g_xbmcController.inputHandler.inputSettings useSiriRemote])
+ {
+ static UIPanGestureRecognizerDirection direction = UIPanGestureRecognizerDirectionUndefined;
+ // speed == how many clicks full swipe will give us(1000x1000px)
+ // minVelocity == min velocity to trigger fast scroll, add this to settings?
+ float speed = 240.0f;
+ float minVelocity = 1300.0f;
+ switch (sender.state)
+ {
+
+ case UIGestureRecognizerStateBegan:
+ {
+
+ if (direction == UIPanGestureRecognizerDirectionUndefined)
+ {
+ m_lastGesturePoint = [sender translationInView:sender.view];
+ m_lastGesturePoint.x = m_lastGesturePoint.x / 1.92;
+ m_lastGesturePoint.y = m_lastGesturePoint.y / 1.08;
+
+ m_direction = [self getPanDirection:m_lastGesturePoint];
+ m_directionOverride = false;
+ }
+ break;
+ }
+ case UIGestureRecognizerStateChanged:
+ {
+ CGPoint gesturePoint = [sender translationInView:sender.view];
+ gesturePoint.x = gesturePoint.x / 1.92;
+ gesturePoint.y = gesturePoint.y / 1.08;
+
+ CGPoint gestureMovement;
+ gestureMovement.x = gesturePoint.x - m_lastGesturePoint.x;
+ gestureMovement.y = gesturePoint.y - m_lastGesturePoint.y;
+ direction = [self getPanDirection:gestureMovement];
+
+ CGPoint velocity = [sender velocityInView:sender.view];
+ CGFloat velocityX = (0.2 * velocity.x);
+ CGFloat velocityY = (0.2 * velocity.y);
+
+ if (ABS(velocityY) > minVelocity || ABS(velocityX) > minVelocity || m_directionOverride)
+ {
+ direction = m_direction;
+ // Override direction to correct swipe errors
+ m_directionOverride = true;
+ }
+ switch (direction)
+ {
+ case UIPanGestureRecognizerDirectionUp:
+ {
+ if ((ABS(m_lastGesturePoint.y - gesturePoint.y) > speed) ||
+ ABS(velocityY) > minVelocity)
+ {
+ [g_xbmcController.inputHandler sendButtonPressed:8];
+ if (ABS(velocityY) > minVelocity && [self shouldFastScroll])
+ [g_xbmcController.inputHandler sendButtonPressed:8];
+
+ m_lastGesturePoint = gesturePoint;
+ }
+ break;
+ }
+ case UIPanGestureRecognizerDirectionDown:
+ {
+ if ((ABS(m_lastGesturePoint.y - gesturePoint.y) > speed) ||
+ ABS(velocityY) > minVelocity)
+ {
+ [g_xbmcController.inputHandler sendButtonPressed:9];
+ if (ABS(velocityY) > minVelocity && [self shouldFastScroll])
+ [g_xbmcController.inputHandler sendButtonPressed:9];
+
+ m_lastGesturePoint = gesturePoint;
+ }
+ break;
+ }
+ case UIPanGestureRecognizerDirectionLeft:
+ {
+ // add 80 px to slow left/right swipes, it matched up down better
+ if ((ABS(m_lastGesturePoint.x - gesturePoint.x) > speed + 80) ||
+ ABS(velocityX) > minVelocity)
+ {
+ [g_xbmcController.inputHandler sendButtonPressed:10];
+ if (ABS(velocityX) > minVelocity && [self shouldFastScroll])
+ [g_xbmcController.inputHandler sendButtonPressed:10];
+
+ m_lastGesturePoint = gesturePoint;
+ }
+ break;
+ }
+ case UIPanGestureRecognizerDirectionRight:
+ {
+ // add 80 px to slow left/right swipes, it matched up down better
+ if ((ABS(m_lastGesturePoint.x - gesturePoint.x) > speed + 80) ||
+ ABS(velocityX) > minVelocity)
+ {
+ [g_xbmcController.inputHandler sendButtonPressed:11];
+ if (ABS(velocityX) > minVelocity && [self shouldFastScroll])
+ [g_xbmcController.inputHandler sendButtonPressed:11];
+
+ m_lastGesturePoint = gesturePoint;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ case UIGestureRecognizerStateEnded:
+ {
+ direction = UIPanGestureRecognizerDirectionUndefined;
+ // start remote idle timer
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ else // dont mimic apple siri remote
+ {
+ switch (sender.state)
+ {
+ case UIGestureRecognizerStateBegan:
+ {
+ m_touchBeginSignaled = false;
+ break;
+ }
+ case UIGestureRecognizerStateChanged:
+ {
+ int keyId = 0;
+ if (!m_touchBeginSignaled && m_touchDirection)
+ {
+ switch (m_touchDirection)
+ {
+ case UISwipeGestureRecognizerDirectionRight:
+ {
+ keyId = 11;
+ break;
+ }
+ case UISwipeGestureRecognizerDirectionLeft:
+ {
+ keyId = 10;
+ break;
+ }
+ case UISwipeGestureRecognizerDirectionUp:
+ {
+ keyId = 8;
+ break;
+ }
+ case UISwipeGestureRecognizerDirectionDown:
+ {
+ keyId = 9;
+ break;
+ }
+ default:
+ break;
+ }
+ m_touchBeginSignaled = true;
+ [g_xbmcController.inputHandler.inputRemote startKeyPressTimer:keyId];
+ }
+ break;
+ }
+ case UIGestureRecognizerStateEnded:
+ case UIGestureRecognizerStateCancelled:
+ {
+ if (m_touchBeginSignaled)
+ {
+ m_touchBeginSignaled = false;
+ m_touchDirection = NULL;
+ [g_xbmcController.inputHandler.inputRemote stopKeyPressTimer];
+ }
+ // start remote idle timer
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+ break;
+ }
+ default:
+ break;
+ }
+ }
+}
+
+- (IBAction)handleSwipe:(UISwipeGestureRecognizer*)sender
+{
+ if (!g_xbmcController.inputHandler.inputRemote.remoteIdleState)
+ m_touchDirection = sender.direction;
+
+ // start remote idle timer
+ [g_xbmcController.inputHandler.inputRemote startRemoteTimer];
+}
+
+- (UIPanGestureRecognizerDirection)getPanDirection:(CGPoint)translation
+{
+ int x = static_cast<int>(translation.x);
+ int y = static_cast<int>(translation.y);
+ int absX = x;
+ int absY = y;
+
+ if (absX < 0)
+ absX *= -1;
+
+ if (absY < 0)
+ absY *= -1;
+
+ bool horizontal, veritical;
+ horizontal = (absX > absY);
+ veritical = !horizontal;
+
+ // Determine up, down, right, or left:
+ bool swipe_up, swipe_down, swipe_left, swipe_right;
+ swipe_left = (horizontal && x < 0);
+ swipe_right = (horizontal && x >= 0);
+ swipe_up = (veritical && y < 0);
+ swipe_down = (veritical && y >= 0);
+
+ if (swipe_down)
+ return UIPanGestureRecognizerDirectionDown;
+ if (swipe_up)
+ return UIPanGestureRecognizerDirectionUp;
+ if (swipe_left)
+ return UIPanGestureRecognizerDirectionLeft;
+ if (swipe_right)
+ return UIPanGestureRecognizerDirectionRight;
+
+ return UIPanGestureRecognizerDirectionUndefined;
+}
+
+#pragma mark - Private Functions
+
+- (BOOL)shouldFastScroll
+{
+ // we dont want fast scroll in below windows, no point in going 15 places in home screen
+ int window = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow();
+
+ if (window == WINDOW_HOME || window == WINDOW_FULLSCREEN_LIVETV ||
+ window == WINDOW_FULLSCREEN_VIDEO || window == WINDOW_FULLSCREEN_RADIO ||
+ (window >= WINDOW_SETTINGS_START && window <= WINDOW_SETTINGS_SERVICE))
+ return NO;
+
+ return YES;
+}
+
+@end
diff --git a/xbmc/platform/darwin/tvos/tvosShared.h b/xbmc/platform/darwin/tvos/tvosShared.h
new file mode 100644
index 0000000000..dd9bbc5279
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/tvosShared.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import <Foundation/Foundation.h>
+
+@interface tvosShared : NSObject
++ (NSString*)getSharedID;
++ (NSURL*)getSharedURL;
++ (BOOL)isJailbroken;
++ (NSBundle*)mainAppBundle;
+@end
diff --git a/xbmc/platform/darwin/tvos/tvosShared.m b/xbmc/platform/darwin/tvos/tvosShared.m
new file mode 100644
index 0000000000..b194dd9f48
--- /dev/null
+++ b/xbmc/platform/darwin/tvos/tvosShared.m
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "tvosShared.h"
+
+@implementation tvosShared
+
++ (NSString*)getSharedID
+{
+ return [@"group." stringByAppendingString:[self mainAppBundle].bundleIdentifier];
+}
+
++ (NSURL*)getSharedURL
+{
+ NSString* sharedID = [self getSharedID];
+ if ([self isJailbroken])
+ return [[NSURL fileURLWithPath:@"/var/mobile/Library/Caches"]
+ URLByAppendingPathComponent:sharedID];
+ else
+ {
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ NSURL* sharedUrl = [fileManager containerURLForSecurityApplicationGroupIdentifier:sharedID];
+ sharedUrl = [sharedUrl URLByAppendingPathComponent:@"Library" isDirectory:YES];
+ sharedUrl = [sharedUrl URLByAppendingPathComponent:@"Caches" isDirectory:YES];
+ return sharedUrl;
+ }
+}
+
++ (BOOL)IsTVOSSandboxed
+{
+ // @todo merge with CDarwinUtils::IsIosSandboxed
+ static BOOL ret;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ // we re NOT sandboxed if we are installed in /var/mobile/Applications with greeng0blin jailbreak
+ ret = ![[self mainAppBundle].bundlePath containsString:@"/var/mobile/Applications/"];
+ });
+ return ret;
+}
+
++ (BOOL)isJailbroken
+{
+ return ![self IsTVOSSandboxed];
+}
+
++ (NSBundle*)mainAppBundle
+{
+ NSBundle* bundle = NSBundle.mainBundle;
+ if ([bundle.bundleURL.pathExtension isEqualToString:@"appex"])
+ { // We're in a extension
+ // Peel off two directory levels - Kodi.app/PlugIns/MY_APP_EXTENSION.appex
+ bundle = [NSBundle bundleWithURL:bundle.bundleURL.URLByDeletingLastPathComponent
+ .URLByDeletingLastPathComponent];
+ }
+ return bundle;
+}
+
+@end
diff --git a/xbmc/settings/DisplaySettings.cpp b/xbmc/settings/DisplaySettings.cpp
index bfce058c6f..da66d859f0 100644
--- a/xbmc/settings/DisplaySettings.cpp
+++ b/xbmc/settings/DisplaySettings.cpp
@@ -47,6 +47,9 @@
#elif defined(TARGET_DARWIN_IOS)
#define WIN_SYSTEM_CLASS CWinSystemIOS
#include "windowing/ios/WinSystemIOS.h"
+#elif defined(TARGET_DARWIN_TVOS)
+#define WIN_SYSTEM_CLASS CWinSystemTVOS
+#include "windowing/tvos/WinSystemTVOS.h"
#elif defined(HAVE_WAYLAND)
#define WIN_SYSTEM_CLASS KODI::WINDOWING::WAYLAND::CWinSystemWayland
#include "windowing/wayland/WinSystemWayland.h"
diff --git a/xbmc/settings/SettingConditions.cpp b/xbmc/settings/SettingConditions.cpp
index 46f45bcb5f..856990e75d 100644
--- a/xbmc/settings/SettingConditions.cpp
+++ b/xbmc/settings/SettingConditions.cpp
@@ -307,6 +307,9 @@ void CSettingConditions::Initialize()
#ifdef TARGET_DARWIN_IOS
m_simpleConditions.insert("have_ios");
#endif
+#ifdef TARGET_DARWIN_TVOS
+ m_simpleConditions.insert("have_tvos");
+#endif
#if defined(TARGET_WINDOWS)
m_simpleConditions.insert("has_dx");
m_simpleConditions.insert("hasdxva2");
diff --git a/xbmc/settings/Settings.cpp b/xbmc/settings/Settings.cpp
index f63b6f7413..e99ec39129 100644
--- a/xbmc/settings/Settings.cpp
+++ b/xbmc/settings/Settings.cpp
@@ -27,6 +27,9 @@
#if defined(TARGET_DARWIN_OSX)
#include "platform/darwin/osx/XBMCHelper.h"
#endif // defined(TARGET_DARWIN_OSX)
+#if defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/TVOSSettingsHandler.h"
+#endif // defined(TARGET_DARWIN_TVOS)
#if defined(TARGET_DARWIN_EMBEDDED)
#include "SettingAddon.h"
#endif
@@ -365,6 +368,11 @@ const std::string CSettings::SETTING_INPUT_CONTROLLERPOWEROFF = "input.controlle
const std::string CSettings::SETTING_INPUT_APPLEREMOTEMODE = "input.appleremotemode";
const std::string CSettings::SETTING_INPUT_APPLEREMOTEALWAYSON = "input.appleremotealwayson";
const std::string CSettings::SETTING_INPUT_APPLEREMOTESEQUENCETIME = "input.appleremotesequencetime";
+const std::string CSettings::SETTING_INPUT_APPLESIRI = "input.applesiri";
+const std::string CSettings::SETTING_INPUT_APPLESIRITIMEOUT = "input.applesiritimeout";
+const std::string CSettings::SETTING_INPUT_APPLESIRITIMEOUTENABLED =
+ "input.applesiritimeoutenabled";
+const std::string CSettings::SETTING_INPUT_APPLEUSEKODIKEYBOARD = "input.appleusekodikeyboard";
const std::string CSettings::SETTING_NETWORK_USEHTTPPROXY = "network.usehttpproxy";
const std::string CSettings::SETTING_NETWORK_HTTPPROXYTYPE = "network.httpproxytype";
const std::string CSettings::SETTING_NETWORK_HTTPPROXYSERVER = "network.httpproxyserver";
@@ -632,6 +640,10 @@ bool CSettings::InitializeDefinitions()
#elif defined(TARGET_DARWIN_IOS)
if (CFile::Exists(SETTINGS_XML_FOLDER "darwin_ios.xml") && !Initialize(SETTINGS_XML_FOLDER "darwin_ios.xml"))
CLog::Log(LOGFATAL, "Unable to load ios-specific settings definitions");
+#elif defined(TARGET_DARWIN_TVOS)
+ if (CFile::Exists(SETTINGS_XML_FOLDER "darwin_tvos.xml") &&
+ !Initialize(SETTINGS_XML_FOLDER "darwin_tvos.xml"))
+ CLog::Log(LOGFATAL, "Unable to load tvos-specific settings definitions");
#endif
#endif
@@ -986,6 +998,14 @@ void CSettings::InitializeISettingCallbacks()
GetSettingsManager()->RegisterCallback(&XBMCHelper::GetInstance(), settingSet);
#endif
+#if defined(TARGET_DARWIN_TVOS)
+ settingSet.clear();
+ settingSet.insert(CSettings::SETTING_INPUT_APPLESIRI);
+ settingSet.insert(CSettings::SETTING_INPUT_APPLESIRITIMEOUT);
+ settingSet.insert(CSettings::SETTING_INPUT_APPLESIRITIMEOUTENABLED);
+ GetSettingsManager()->RegisterCallback(&CTVOSInputSettings::GetInstance(), settingSet);
+#endif
+
settingSet.clear();
settingSet.insert(CSettings::SETTING_ADDONS_SHOW_RUNNING);
settingSet.insert(CSettings::SETTING_ADDONS_MANAGE_DEPENDENCIES);
diff --git a/xbmc/settings/Settings.h b/xbmc/settings/Settings.h
index a0e10a4c26..e20b0233ac 100644
--- a/xbmc/settings/Settings.h
+++ b/xbmc/settings/Settings.h
@@ -333,6 +333,10 @@ public:
static const std::string SETTING_INPUT_APPLEREMOTEMODE;
static const std::string SETTING_INPUT_APPLEREMOTEALWAYSON;
static const std::string SETTING_INPUT_APPLEREMOTESEQUENCETIME;
+ static const std::string SETTING_INPUT_APPLESIRI;
+ static const std::string SETTING_INPUT_APPLESIRITIMEOUT;
+ static const std::string SETTING_INPUT_APPLESIRITIMEOUTENABLED;
+ static const std::string SETTING_INPUT_APPLEUSEKODIKEYBOARD;
static const std::string SETTING_NETWORK_USEHTTPPROXY;
static const std::string SETTING_NETWORK_HTTPPROXYTYPE;
static const std::string SETTING_NETWORK_HTTPPROXYSERVER;
diff --git a/xbmc/threads/platform/pthreads/ThreadImpl.cpp b/xbmc/threads/platform/pthreads/ThreadImpl.cpp
index 5e9eb453ef..58d9945ff0 100644
--- a/xbmc/threads/platform/pthreads/ThreadImpl.cpp
+++ b/xbmc/threads/platform/pthreads/ThreadImpl.cpp
@@ -59,6 +59,8 @@ static pid_t GetCurrentThreadPid_()
return pthread_getthreadid_np();
#elif defined(TARGET_ANDROID)
return gettid();
+#elif TARGET_DARWIN_TVOS
+ return pthread_mach_thread_np(pthread_self());
#else
return syscall(SYS_gettid);
#endif
diff --git a/xbmc/utils/RecentlyAddedJob.cpp b/xbmc/utils/RecentlyAddedJob.cpp
index bdeb7cb604..c35e9ae295 100644
--- a/xbmc/utils/RecentlyAddedJob.cpp
+++ b/xbmc/utils/RecentlyAddedJob.cpp
@@ -26,6 +26,10 @@
#include "video/VideoInfoTag.h"
#include "video/VideoThumbLoader.h"
+#if defined(TARGET_DARWIN_TVOS)
+#include "platform/darwin/tvos/TVOSTopShelf.h"
+#endif
+
#define NUM_ITEMS 10
CRecentlyAddedJob::CRecentlyAddedJob(int flag)
@@ -140,6 +144,11 @@ bool CRecentlyAddedJob::UpdateVideo()
home->SetProperty("LatestEpisode." + value + ".Fanart" , "");
}
+#if defined(TARGET_DARWIN_TVOS)
+ // send recently added Movies and TvShows to TopShelf
+ CTVOSTopShelf::GetInstance().SetTopShelfItems(items, TVShowItems);
+#endif
+
i = 0;
CFileItemList MusicVideoItems;
diff --git a/xbmc/utils/SystemInfo.cpp b/xbmc/utils/SystemInfo.cpp
index eb63e7c589..a85c10d185 100644
--- a/xbmc/utils/SystemInfo.cpp
+++ b/xbmc/utils/SystemInfo.cpp
@@ -557,6 +557,8 @@ std::string CSysInfo::GetOsName(bool emptyIfUnknown /* = false*/)
osName = GetKernelName(true); // FIXME: for FreeBSD OS name is a kernel name
#elif defined(TARGET_DARWIN_IOS)
osName = "iOS";
+#elif defined(TARGET_DARWIN_TVOS)
+ osName = "tvOS";
#elif defined(TARGET_DARWIN_OSX)
osName = "OS X";
#elif defined (TARGET_ANDROID)
@@ -1058,8 +1060,18 @@ std::string CSysInfo::GetUserAgent()
&& iOSVersion.find_first_not_of('0', lastDotPos + 1) == std::string::npos)
iOSVersion.erase(lastDotPos);
StringUtils::Replace(iOSVersion, '.', '_');
- if (iDev == "iPad" || iDev == "AppleTV")
- result += "CPU OS ";
+ if (iDev == "AppleTV")
+ {
+ // check if it's ATV4 (AppleTV5,3) or later
+ auto modelMajorNumberEndPos = iDevStr.find_first_of(',', iDevStrDigit);
+ std::string s{iDevStr, iDevStrDigit, modelMajorNumberEndPos - iDevStrDigit};
+ if (stoi(s) >= 5)
+ result += "CPU TVOS";
+ else
+ result += "CPU OS";
+ }
+ else if (iDev == "iPad")
+ result += "CPU OS";
else
result += "CPU iPhone OS ";
result += iOSVersion + " like Mac OS X";
@@ -1199,6 +1211,8 @@ std::string CSysInfo::GetBuildTargetPlatformName(void)
return "OS X";
#elif defined(TARGET_DARWIN_IOS)
return "iOS";
+#elif defined(TARGET_DARWIN_TVOS)
+ return "tvOS";
#elif defined(TARGET_FREEBSD)
return "FreeBSD";
#elif defined(TARGET_ANDROID)
@@ -1222,6 +1236,8 @@ std::string CSysInfo::GetBuildTargetPlatformVersion(void)
return XSTR_MACRO(__MAC_OS_X_VERSION_MIN_REQUIRED);
#elif defined(TARGET_DARWIN_IOS)
return XSTR_MACRO(__IPHONE_OS_VERSION_MIN_REQUIRED);
+#elif defined(TARGET_DARWIN_TVOS)
+ return XSTR_MACRO(__TV_OS_VERSION_MIN_REQUIRED);
#elif defined(TARGET_FREEBSD)
return XSTR_MACRO(__FreeBSD_version);
#elif defined(TARGET_ANDROID)
diff --git a/xbmc/utils/test/TestSystemInfo.cpp b/xbmc/utils/test/TestSystemInfo.cpp
index a42a68c596..b80d32bc33 100644
--- a/xbmc/utils/test/TestSystemInfo.cpp
+++ b/xbmc/utils/test/TestSystemInfo.cpp
@@ -102,6 +102,10 @@ TEST_F(TestSystemInfo, GetOsName)
#elif defined(TARGET_DARWIN_IOS)
EXPECT_STREQ("iOS", g_sysinfo.GetOsName(true).c_str()) << "'GetOsName(true)' must return 'iOS'";
EXPECT_STREQ("iOS", g_sysinfo.GetOsName(false).c_str()) << "'GetOsName(false)' must return 'iOS'";
+#elif defined(TARGET_DARWIN_TVOS)
+ EXPECT_STREQ("tvOS", g_sysinfo.GetOsName(true).c_str()) << "'GetOsName(true)' must return 'tvOS'";
+ EXPECT_STREQ("tvOS", g_sysinfo.GetOsName(false).c_str())
+ << "'GetOsName(false)' must return 'tvOS'";
#elif defined(TARGET_DARWIN_OSX)
EXPECT_STREQ("OS X", g_sysinfo.GetOsName(true).c_str()) << "'GetOsName(true)' must return 'OS X'";
EXPECT_STREQ("OS X", g_sysinfo.GetOsName(false).c_str()) << "'GetOsName(false)' must return 'OS X'";
@@ -217,6 +221,11 @@ TEST_F(TestSystemInfo, GetUserAgent)
#elif defined(TARGET_DARWIN_IOS)
EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find("like Mac OS X")) << "'GetUserAgent()' must contain ' like Mac OS X'";
EXPECT_TRUE(g_sysinfo.GetUserAgent().find("CPU OS ") != std::string::npos || g_sysinfo.GetUserAgent().find("CPU iPhone OS ") != std::string::npos) << "'GetUserAgent()' must contain 'CPU OS ' or 'CPU iPhone OS '";
+#elif defined(TARGET_DARWIN_TVOS)
+ EXPECT_NE(std::string::npos, g_sysinfo.GetUserAgent().find("like Mac OS X"))
+ << "'GetUserAgent()' must contain ' like Mac OS X'";
+ EXPECT_TRUE(g_sysinfo.GetUserAgent().find("CPU TVOS ") != std::string::npos)
+ << "'GetUserAgent()' must contain 'CPU TVOS '";
#elif defined(TARGET_DARWIN_OSX)
EXPECT_EQ(g_sysinfo.GetUserAgent().find('('), g_sysinfo.GetUserAgent().find("(Macintosh; ")) << "Second parameter in 'GetUserAgent()' string must start from 'Macintosh; '";
#elif defined(TARGET_ANDROID)
diff --git a/xbmc/windowing/tvos/CMakeLists.txt b/xbmc/windowing/tvos/CMakeLists.txt
new file mode 100644
index 0000000000..d9aa9cc191
--- /dev/null
+++ b/xbmc/windowing/tvos/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(SOURCES OSScreenSaverTVOS.mm
+ WinEventsTVOS.mm
+ WinSystemTVOS.mm
+ VideoSyncTVos.cpp)
+set(HEADERS OSScreenSaverTVOS.h
+ WinEventsTVOS.h
+ WinSystemTVOS.h
+ VideoSyncTVos.h)
+
+core_add_library(windowing_tvos)
diff --git a/xbmc/windowing/tvos/OSScreenSaverTVOS.h b/xbmc/windowing/tvos/OSScreenSaverTVOS.h
new file mode 100644
index 0000000000..2b5d3bd6bb
--- /dev/null
+++ b/xbmc/windowing/tvos/OSScreenSaverTVOS.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2017-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "windowing/OSScreenSaver.h"
+
+class COSScreenSaverTVOS : public KODI::WINDOWING::IOSScreenSaver
+{
+public:
+ COSScreenSaverTVOS() = default;
+ void Inhibit() override;
+ void Uninhibit() override;
+};
diff --git a/xbmc/windowing/tvos/OSScreenSaverTVOS.mm b/xbmc/windowing/tvos/OSScreenSaverTVOS.mm
new file mode 100644
index 0000000000..7af5580b30
--- /dev/null
+++ b/xbmc/windowing/tvos/OSScreenSaverTVOS.mm
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2017-2019 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#import "OSScreenSaverTVOS.h"
+
+#import "platform/darwin/tvos/XBMCController.h"
+
+void COSScreenSaverTVOS::Inhibit()
+{
+ [g_xbmcController disableScreenSaver];
+}
+
+void COSScreenSaverTVOS::Uninhibit()
+{
+ [g_xbmcController enableScreenSaver];
+}
diff --git a/xbmc/windowing/tvos/VideoSyncTVos.cpp b/xbmc/windowing/tvos/VideoSyncTVos.cpp
new file mode 100644
index 0000000000..eee802645c
--- /dev/null
+++ b/xbmc/windowing/tvos/VideoSyncTVos.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "VideoSyncTVos.h"
+
+#include "cores/VideoPlayer/VideoReferenceClock.h"
+#include "utils/MathUtils.h"
+#include "utils/TimeUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#import "windowing/tvos/WinSystemTVOS.h"
+
+bool CVideoSyncTVos::Setup(PUPDATECLOCK func)
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncTVos::{} setting up TVOS", __FUNCTION__);
+
+ //init the vblank timestamp
+ m_LastVBlankTime = CurrentHostCounter();
+ UpdateClock = func;
+ m_abortEvent.Reset();
+
+ bool setupOk = InitDisplayLink();
+ if (setupOk)
+ {
+ m_winSystem.Register(this);
+ }
+
+ return setupOk;
+}
+
+void CVideoSyncTVos::Run(CEvent& stopEvent)
+{
+ //because cocoa has a vblank callback, we just keep sleeping until we're asked to stop the thread
+ XbmcThreads::CEventGroup waitGroup{&stopEvent, &m_abortEvent};
+ waitGroup.wait();
+}
+
+void CVideoSyncTVos::Cleanup()
+{
+ CLog::Log(LOGDEBUG, "CVideoSyncTVos::{} cleaning up TVOS", __FUNCTION__);
+ DeinitDisplayLink();
+ m_winSystem.Unregister(this);
+}
+
+float CVideoSyncTVos::GetFps()
+{
+ m_fps = CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS();
+ CLog::Log(LOGDEBUG, "CVideoSyncTVos::{} Detected refreshrate: {} hertz", __FUNCTION__, m_fps);
+ return m_fps;
+}
+
+void CVideoSyncTVos::OnResetDisplay()
+{
+ m_abortEvent.Set();
+}
+
+void CVideoSyncTVos::TVosVblankHandler()
+{
+ int64_t nowtime = CurrentHostCounter();
+
+ //calculate how many vblanks happened
+ double VBlankTime =
+ static_cast<double>(nowtime - m_LastVBlankTime) / static_cast<double>(CurrentHostFrequency());
+ int NrVBlanks = MathUtils::round_int(VBlankTime * m_fps);
+
+ //save the timestamp of this vblank so we can calculate how many happened next time
+ m_LastVBlankTime = nowtime;
+
+ //update the vblank timestamp, update the clock and send a signal that we got a vblank
+ UpdateClock(NrVBlanks, nowtime, m_refClock);
+}
+
+bool CVideoSyncTVos::InitDisplayLink()
+{
+ bool ret = true;
+ CLog::Log(LOGDEBUG, "CVideoSyncTVos: setting up displaylink");
+ if (!m_winSystem.InitDisplayLink(this))
+ {
+ CLog::Log(LOGDEBUG, "CVideoSyncTVos: InitDisplayLink failed");
+ ret = false;
+ }
+ return ret;
+}
+
+void CVideoSyncTVos::DeinitDisplayLink()
+{
+ m_winSystem.DeinitDisplayLink();
+}
diff --git a/xbmc/windowing/tvos/VideoSyncTVos.h b/xbmc/windowing/tvos/VideoSyncTVos.h
new file mode 100644
index 0000000000..93f17b8e4b
--- /dev/null
+++ b/xbmc/windowing/tvos/VideoSyncTVos.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "guilib/DispResource.h"
+#include "windowing/VideoSync.h"
+
+class CWinSystemTVOS;
+
+class CVideoSyncTVos : public CVideoSync, IDispResource
+{
+public:
+ CVideoSyncTVos(void* clock, CWinSystemTVOS& winSystem)
+ : CVideoSync(clock), m_winSystem(winSystem){};
+
+ // CVideoSync interface
+ bool Setup(PUPDATECLOCK func) override;
+ void Run(CEvent& stopEvent) override;
+ void Cleanup() override;
+ float GetFps() override;
+
+ // IDispResource interface
+ void OnResetDisplay() override;
+
+ // used in the displaylink callback
+ void TVosVblankHandler();
+
+private:
+ // CVideoSyncDarwin interface
+ virtual bool InitDisplayLink();
+ virtual void DeinitDisplayLink();
+
+ //timestamp of the last vblank, used for calculating how many vblanks happened
+ int64_t m_LastVBlankTime = 0;
+ CEvent m_abortEvent;
+ CWinSystemTVOS& m_winSystem;
+};
diff --git a/xbmc/windowing/tvos/WinEventsTVOS.h b/xbmc/windowing/tvos/WinEventsTVOS.h
new file mode 100644
index 0000000000..fa30665da6
--- /dev/null
+++ b/xbmc/windowing/tvos/WinEventsTVOS.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "threads/CriticalSection.h"
+#include "threads/Event.h"
+#include "threads/Thread.h"
+#include "windowing/WinEvents.h"
+
+#include <list>
+#include <queue>
+#include <string>
+#include <vector>
+
+class CWinEventsTVOS : public IWinEvents, public CThread
+{
+public:
+ CWinEventsTVOS();
+ ~CWinEventsTVOS();
+
+ void MessagePush(XBMC_Event* newEvent);
+ size_t GetQueueSize();
+
+ bool MessagePump();
+
+private:
+ CCriticalSection m_eventsCond;
+ std::list<XBMC_Event> m_events;
+};
diff --git a/xbmc/windowing/tvos/WinEventsTVOS.mm b/xbmc/windowing/tvos/WinEventsTVOS.mm
new file mode 100644
index 0000000000..68cb7d2a1f
--- /dev/null
+++ b/xbmc/windowing/tvos/WinEventsTVOS.mm
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+
+#include "WinEventsTVOS.h"
+
+#include "AppInboundProtocol.h"
+#include "ServiceBroker.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/XBMC_vkeys.h"
+#include "threads/CriticalSection.h"
+#include "utils/log.h"
+
+#include <list>
+
+static CCriticalSection g_inputCond;
+
+static std::list<XBMC_Event> events;
+
+CWinEventsTVOS::CWinEventsTVOS() : CThread("CWinEventsTVOS")
+{
+ CLog::Log(LOGDEBUG, "CWinEventsTVOS::CWinEventsTVOS");
+ Create();
+}
+
+CWinEventsTVOS::~CWinEventsTVOS()
+{
+ m_bStop = true;
+ StopThread(true);
+}
+
+void CWinEventsTVOS::MessagePush(XBMC_Event* newEvent)
+{
+ CSingleLock lock(m_eventsCond);
+
+ m_events.push_back(*newEvent);
+}
+
+size_t CWinEventsTVOS::GetQueueSize()
+{
+ CSingleLock lock(g_inputCond);
+ return events.size();
+}
+
+
+bool CWinEventsTVOS::MessagePump()
+{
+ bool ret = false;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+
+ // Do not always loop, only pump the initial queued count events. else if ui keep pushing
+ // events the loop won't finish then it will block xbmc main message loop.
+ for (size_t pumpEventCount = GetQueueSize(); pumpEventCount > 0; --pumpEventCount)
+ {
+ // Pop up only one event per time since in App::OnEvent it may init modal dialog which init
+ // deeper message loop and call the deeper MessagePump from there.
+ XBMC_Event pumpEvent;
+ {
+ CSingleLock lock(g_inputCond);
+ if (events.empty())
+ return ret;
+ pumpEvent = events.front();
+ events.pop_front();
+ }
+
+ if (appPort)
+ ret = appPort->OnEvent(pumpEvent);
+ }
+ return ret;
+}
diff --git a/xbmc/windowing/tvos/WinSystemTVOS.h b/xbmc/windowing/tvos/WinSystemTVOS.h
new file mode 100644
index 0000000000..c03e46803d
--- /dev/null
+++ b/xbmc/windowing/tvos/WinSystemTVOS.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#pragma once
+
+#include "rendering/gles/RenderSystemGLES.h"
+#include "threads/CriticalSection.h"
+#include "threads/Timer.h"
+#include "windowing/OSScreenSaver.h"
+#include "windowing/WinSystem.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <CoreVideo/CVOpenGLESTextureCache.h>
+
+class IDispResource;
+class CVideoSyncTVos;
+struct CADisplayLinkWrapper;
+
+class CWinSystemTVOS : public CWinSystemBase, public CRenderSystemGLES, public ITimerCallback
+{
+public:
+ CWinSystemTVOS();
+ virtual ~CWinSystemTVOS();
+
+ // ITimerCallback interface
+ virtual void OnTimeout() override {}
+
+ void MessagePush(XBMC_Event* newEvent);
+ size_t GetQueueSize();
+ void AnnounceOnLostDevice();
+ void AnnounceOnResetDevice();
+ void StartLostDeviceTimer();
+ void StopLostDeviceTimer();
+ int GetDisplayIndexFromSettings();
+ // Implementation of CWinSystemBase
+ CRenderSystemBase* GetRenderSystem() override { return this; }
+ bool InitWindowSystem() override;
+ bool DestroyWindowSystem() override;
+ bool CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res) override;
+ bool DestroyWindow() override;
+ bool ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop) override;
+ bool SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays) override;
+ void UpdateResolutions() override;
+ bool CanDoWindowed() override { return false; }
+
+ void ShowOSMouse(bool show) override {}
+ bool HasCursor() override;
+
+ void NotifyAppActiveChange(bool bActivated) override;
+
+ bool Minimize() override;
+ bool Restore() override;
+ bool Hide() override;
+ bool Show(bool raise = true) override;
+
+ bool IsExtSupported(const char* extension) const override;
+
+ bool BeginRender() override;
+ bool EndRender() override;
+
+ void Register(IDispResource* resource) override;
+ void Unregister(IDispResource* resource) override;
+
+ //virtual std::unique_ptr<CVideoSync> GetVideoSync(void* clock) override;
+
+ bool InitDisplayLink(CVideoSyncTVos* syncImpl);
+ void DeinitDisplayLink(void);
+ void OnAppFocusChange(bool focus);
+ bool IsBackgrounded() const { return m_bIsBackgrounded; }
+ CVEAGLContext GetEAGLContextObj();
+ void GetConnectedOutputs(std::vector<std::string>* outputs);
+
+ // winevents override
+ bool MessagePump() override;
+
+protected:
+ virtual std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> GetOSScreenSaverImpl() override;
+ void PresentRenderImpl(bool rendered) override;
+ void SetVSyncImpl(bool enable) override {}
+
+ void* m_glView; // EAGLView opaque
+ void* m_WorkingContext; // shared EAGLContext opaque
+ bool m_bWasFullScreenBeforeMinimize;
+ std::string m_eglext;
+ CCriticalSection m_resourceSection;
+ std::vector<IDispResource*> m_resources;
+ bool m_bIsBackgrounded;
+ CTimer m_lostDeviceTimer;
+
+private:
+ bool GetScreenResolution(int* w, int* h, double* fps, int screenIdx);
+ void FillInVideoModes(int screenIdx);
+ bool SwitchToVideoMode(int width, int height, double refreshrate);
+ CADisplayLinkWrapper* m_pDisplayLink;
+};
diff --git a/xbmc/windowing/tvos/WinSystemTVOS.mm b/xbmc/windowing/tvos/WinSystemTVOS.mm
new file mode 100644
index 0000000000..8c2fe096e6
--- /dev/null
+++ b/xbmc/windowing/tvos/WinSystemTVOS.mm
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2010-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinSystemTVOS.h"
+
+#import "cores/AudioEngine/Sinks/AESinkDARWINTVOS.h"
+#include "cores/RetroPlayer/process/ios/RPProcessInfoIOS.h"
+#include "cores/RetroPlayer/rendering/VideoRenderers/RPRendererOpenGLES.h"
+#include "cores/VideoPlayer/DVDCodecs/DVDFactoryCodec.h"
+#include "cores/VideoPlayer/DVDCodecs/Video/VTB.h"
+#include "cores/VideoPlayer/Process/ios/ProcessInfoIOS.h"
+#include "cores/VideoPlayer/VideoRenderers/HwDecRender/RendererVTBGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/LinuxRendererGLES.h"
+#include "cores/VideoPlayer/VideoRenderers/RenderFactory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/DispResource.h"
+#include "guilib/Texture.h"
+#include "messaging/ApplicationMessenger.h"
+#include "settings/DisplaySettings.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "threads/SingleLock.h"
+#include "utils/StringUtils.h"
+#include "utils/log.h"
+#include "windowing/GraphicContext.h"
+#include "windowing/OSScreenSaver.h"
+#import "windowing/tvos/OSScreenSaverTVOS.h"
+#import "windowing/tvos/VideoSyncTVos.h"
+#import "windowing/tvos/WinEventsTVOS.h"
+
+#import "platform/darwin/DarwinUtils.h"
+#import "platform/darwin/tvos/TVOSDisplayManager.h"
+#import "platform/darwin/tvos/XBMCController.h"
+
+#include <memory>
+#include <vector>
+
+#import <Foundation/Foundation.h>
+#import <OpenGLES/ES2/gl.h>
+#import <OpenGLES/ES2/glext.h>
+#import <QuartzCore/CADisplayLink.h>
+
+#define CONST_HDMI "HDMI"
+
+// if there was a devicelost callback
+// but no device reset for 3 secs
+// a timeout fires the reset callback
+// (for ensuring that e.x. AE isn't stuck)
+constexpr uint32_t LOST_DEVICE_TIMEOUT_MS{3000};
+
+// TVOSDisplayLinkCallback is defined in the lower part of the file
+@interface TVOSDisplayLinkCallback : NSObject
+{
+@private
+ CVideoSyncTVos* videoSyncImpl;
+}
+@property(nonatomic, setter=SetVideoSyncImpl:) CVideoSyncTVos* videoSyncImpl;
+- (void)runDisplayLink;
+@end
+
+using namespace KODI;
+using namespace MESSAGING;
+
+struct CADisplayLinkWrapper
+{
+ CADisplayLink* impl;
+ TVOSDisplayLinkCallback* callbackClass;
+};
+
+std::unique_ptr<CWinSystemBase> CWinSystemBase::CreateWinSystem()
+{
+ std::unique_ptr<CWinSystemBase> winSystem = std::make_unique<CWinSystemTVOS>();
+ return winSystem;
+}
+
+void CWinSystemTVOS::MessagePush(XBMC_Event* newEvent)
+{
+ dynamic_cast<CWinEventsTVOS&>(*m_winEvents).MessagePush(newEvent);
+}
+
+size_t CWinSystemTVOS::GetQueueSize()
+{
+ return dynamic_cast<CWinEventsTVOS&>(*m_winEvents).GetQueueSize();
+}
+
+void CWinSystemTVOS::AnnounceOnLostDevice()
+{
+ CSingleLock lock(m_resourceSection);
+ // tell any shared resources
+ CLog::Log(LOGDEBUG, "CWinSystemTVOS::AnnounceOnLostDevice");
+ for (auto dispResource : m_resources)
+ dispResource->OnLostDisplay();
+}
+
+void CWinSystemTVOS::AnnounceOnResetDevice()
+{
+ CSingleLock lock(m_resourceSection);
+ // tell any shared resources
+ CLog::Log(LOGDEBUG, "CWinSystemTVOS::AnnounceOnResetDevice");
+ for (auto dispResource : m_resources)
+ dispResource->OnResetDisplay();
+}
+
+void CWinSystemTVOS::StartLostDeviceTimer()
+{
+ if (m_lostDeviceTimer.IsRunning())
+ m_lostDeviceTimer.Restart();
+ else
+ m_lostDeviceTimer.Start(LOST_DEVICE_TIMEOUT_MS, false);
+}
+
+void CWinSystemTVOS::StopLostDeviceTimer()
+{
+ m_lostDeviceTimer.Stop();
+}
+
+
+int CWinSystemTVOS::GetDisplayIndexFromSettings()
+{
+ // ATV only supports 1 screen with id = 0
+ return 0;
+}
+
+CWinSystemTVOS::CWinSystemTVOS() : CWinSystemBase(), m_lostDeviceTimer(this)
+{
+ m_bIsBackgrounded = false;
+ m_pDisplayLink = new CADisplayLinkWrapper;
+ m_pDisplayLink->callbackClass = [[TVOSDisplayLinkCallback alloc] init];
+
+ m_winEvents.reset(new CWinEventsTVOS());
+
+ CAESinkDARWINTVOS::Register();
+}
+
+CWinSystemTVOS::~CWinSystemTVOS()
+{
+ m_pDisplayLink->callbackClass = nil;
+ delete m_pDisplayLink;
+}
+
+bool CWinSystemTVOS::InitWindowSystem()
+{
+ return CWinSystemBase::InitWindowSystem();
+}
+
+bool CWinSystemTVOS::DestroyWindowSystem()
+{
+ return true;
+}
+
+std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> CWinSystemTVOS::GetOSScreenSaverImpl()
+{
+ std::unique_ptr<KODI::WINDOWING::IOSScreenSaver> screensaver =
+ std::make_unique<COSScreenSaverTVOS>();
+ return screensaver;
+}
+
+bool CWinSystemTVOS::CreateNewWindow(const std::string& name, bool fullScreen, RESOLUTION_INFO& res)
+{
+ if (!SetFullScreen(fullScreen, res, false))
+ return false;
+
+ [g_xbmcController setFramebuffer];
+
+ m_bWindowCreated = true;
+
+ m_eglext = " ";
+
+ const char* tmpExtensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
+ if (tmpExtensions != nullptr)
+ {
+ m_eglext += tmpExtensions;
+ m_eglext += " ";
+ }
+
+ CLog::Log(LOGDEBUG, "EGL_EXTENSIONS: {}", m_eglext.c_str());
+
+ // register platform dependent objects
+ CDVDFactoryCodec::ClearHWAccels();
+ VTB::CDecoder::Register();
+ VIDEOPLAYER::CRendererFactory::ClearRenderer();
+ CLinuxRendererGLES::Register();
+ CRendererVTB::Register();
+ VIDEOPLAYER::CProcessInfoIOS::Register();
+ RETRO::CRPProcessInfoIOS::Register();
+ RETRO::CRPProcessInfoIOS::RegisterRendererFactory(new RETRO::CRendererFactoryOpenGLES);
+
+ return true;
+}
+
+bool CWinSystemTVOS::DestroyWindow()
+{
+ return true;
+}
+
+bool CWinSystemTVOS::ResizeWindow(int newWidth, int newHeight, int newLeft, int newTop)
+{
+ if (m_nWidth != newWidth || m_nHeight != newHeight)
+ {
+ m_nWidth = newWidth;
+ m_nHeight = newHeight;
+ }
+
+ CRenderSystemGLES::ResetRenderSystem(newWidth, newHeight);
+
+ return true;
+}
+
+bool CWinSystemTVOS::SetFullScreen(bool fullScreen, RESOLUTION_INFO& res, bool blankOtherDisplays)
+{
+ m_nWidth = res.iWidth;
+ m_nHeight = res.iHeight;
+ m_bFullScreen = fullScreen;
+
+ CLog::Log(LOGDEBUG, "About to switch to {} x {} @ {}", m_nWidth, m_nHeight, res.fRefreshRate);
+ SwitchToVideoMode(res.iWidth, res.iHeight, res.fRefreshRate);
+ CRenderSystemGLES::ResetRenderSystem(res.iWidth, res.iHeight);
+
+ return true;
+}
+
+bool CWinSystemTVOS::SwitchToVideoMode(int width, int height, double refreshrate)
+{
+ /*! @todo Currently support SDR dynamic range only. HDR shouldnt be done during a
+ * modeswitch. Look to create supplemental method to handle sdr/hdr enable
+ */
+ [g_xbmcController.displayManager displayRateSwitch:refreshrate
+ withDynamicRange:0 /*dynamicRange*/];
+ return true;
+}
+
+bool CWinSystemTVOS::GetScreenResolution(int* w, int* h, double* fps, int screenIdx)
+{
+ *w = [g_xbmcController.displayManager getScreenSize].width;
+ *h = [g_xbmcController.displayManager getScreenSize].height;
+ *fps = [g_xbmcController.displayManager getDisplayRate];
+
+ CLog::Log(LOGDEBUG, "Current resolution Screen: {} with {} x {} @ {}", screenIdx, *w, *h, *fps);
+ return true;
+}
+
+void CWinSystemTVOS::UpdateResolutions()
+{
+ // Add display resolution
+ int w, h;
+ double fps;
+ CWinSystemBase::UpdateResolutions();
+
+ int screenIdx = GetDisplayIndexFromSettings();
+
+ //first screen goes into the current desktop mode
+ if (GetScreenResolution(&w, &h, &fps, screenIdx))
+ UpdateDesktopResolution(CDisplaySettings::GetInstance().GetResolutionInfo(RES_DESKTOP),
+ CONST_HDMI, w, h, fps, 0);
+
+ CDisplaySettings::GetInstance().ClearCustomResolutions();
+
+ //now just fill in the possible resolutions for the attached screens
+ //and push to the resolution info vector
+ FillInVideoModes(screenIdx);
+}
+
+void CWinSystemTVOS::FillInVideoModes(int screenIdx)
+{
+ // Potential refresh rates
+ std::vector<float> supportedDispRefreshRates = {23.976f, 24.000f, 25.000f, 29.970f,
+ 30.000f, 50.000f, 59.940f, 60.000f};
+
+ UIScreen* aScreen = UIScreen.screens[screenIdx];
+ UIScreenMode* mode = aScreen.currentMode;
+ int w = mode.size.width;
+ int h = mode.size.height;
+
+ //! @Todo handle different resolutions than native (ie 720p/1080p on a 4k display)
+
+ for (float refreshrate : supportedDispRefreshRates)
+ {
+ RESOLUTION_INFO res;
+ UpdateDesktopResolution(res, CONST_HDMI, w, h, refreshrate, 0);
+ CLog::Log(LOGNOTICE, "Found possible resolution for display {} with {} x {} RefreshRate:{} \n",
+ screenIdx, w, h, refreshrate);
+
+ CServiceBroker::GetWinSystem()->GetGfxContext().ResetOverscan(res);
+ CDisplaySettings::GetInstance().AddResolutionInfo(res);
+ }
+}
+
+bool CWinSystemTVOS::IsExtSupported(const char* extension) const
+{
+ if (strncmp(extension, "EGL_", 4) != 0)
+ return CRenderSystemGLES::IsExtSupported(extension);
+
+ std::string name = ' ' + std::string(extension) + ' ';
+
+ return m_eglext.find(name) != std::string::npos;
+}
+
+
+bool CWinSystemTVOS::BeginRender()
+{
+ bool rtn;
+
+ [g_xbmcController setFramebuffer];
+
+ rtn = CRenderSystemGLES::BeginRender();
+ return rtn;
+}
+
+bool CWinSystemTVOS::EndRender()
+{
+ bool rtn;
+
+ rtn = CRenderSystemGLES::EndRender();
+ return rtn;
+}
+
+void CWinSystemTVOS::Register(IDispResource* resource)
+{
+ CSingleLock lock(m_resourceSection);
+ m_resources.push_back(resource);
+}
+
+void CWinSystemTVOS::Unregister(IDispResource* resource)
+{
+ CSingleLock lock(m_resourceSection);
+ std::vector<IDispResource*>::iterator i = find(m_resources.begin(), m_resources.end(), resource);
+ if (i != m_resources.end())
+ m_resources.erase(i);
+}
+
+void CWinSystemTVOS::OnAppFocusChange(bool focus)
+{
+ CSingleLock lock(m_resourceSection);
+ m_bIsBackgrounded = !focus;
+ CLog::Log(LOGDEBUG, "CWinSystemTVOS::OnAppFocusChange: %d", focus ? 1 : 0);
+ for (auto dispResource : m_resources)
+ dispResource->OnAppFocusChange(focus);
+}
+
+//--------------------------------------------------------------
+//-------------------DisplayLink stuff
+@implementation TVOSDisplayLinkCallback
+@synthesize videoSyncImpl;
+//--------------------------------------------------------------
+- (void)runDisplayLink
+{
+ @autoreleasepool
+ {
+ if (videoSyncImpl != nullptr)
+ videoSyncImpl->TVosVblankHandler();
+ }
+}
+@end
+
+bool CWinSystemTVOS::InitDisplayLink(CVideoSyncTVos* syncImpl)
+{
+ unsigned int currentScreenIdx = GetDisplayIndexFromSettings();
+ UIScreen* currentScreen = UIScreen.screens[currentScreenIdx];
+ m_pDisplayLink->callbackClass.videoSyncImpl = syncImpl;
+ m_pDisplayLink->impl = [currentScreen displayLinkWithTarget:m_pDisplayLink->callbackClass
+ selector:@selector(runDisplayLink)];
+
+ [m_pDisplayLink->impl addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
+ return m_pDisplayLink->impl != nil;
+}
+
+void CWinSystemTVOS::DeinitDisplayLink(void)
+{
+ if (m_pDisplayLink->impl)
+ {
+ [m_pDisplayLink->impl invalidate];
+ m_pDisplayLink->impl = nil;
+ [m_pDisplayLink->callbackClass SetVideoSyncImpl:nil];
+ }
+}
+//------------DisplayLink stuff end
+//--------------------------------------------------------------
+
+void CWinSystemTVOS::PresentRenderImpl(bool rendered)
+{
+ //glFlush;
+ if (rendered)
+ [g_xbmcController presentFramebuffer];
+}
+
+bool CWinSystemTVOS::HasCursor()
+{
+ return false;
+}
+
+void CWinSystemTVOS::NotifyAppActiveChange(bool bActivated)
+{
+ if (bActivated && m_bWasFullScreenBeforeMinimize &&
+ !CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot())
+ CApplicationMessenger::GetInstance().PostMsg(TMSG_TOGGLEFULLSCREEN);
+}
+
+bool CWinSystemTVOS::Minimize()
+{
+ m_bWasFullScreenBeforeMinimize =
+ CServiceBroker::GetWinSystem()->GetGfxContext().IsFullScreenRoot();
+ if (m_bWasFullScreenBeforeMinimize)
+ CApplicationMessenger::GetInstance().PostMsg(TMSG_TOGGLEFULLSCREEN);
+
+ return true;
+}
+
+bool CWinSystemTVOS::Restore()
+{
+ return false;
+}
+
+bool CWinSystemTVOS::Hide()
+{
+ return true;
+}
+
+bool CWinSystemTVOS::Show(bool raise)
+{
+ return true;
+}
+
+CVEAGLContext CWinSystemTVOS::GetEAGLContextObj()
+{
+ return [g_xbmcController getEAGLContextObj];
+}
+
+void CWinSystemTVOS::GetConnectedOutputs(std::vector<std::string>* outputs)
+{
+ outputs->push_back("Default");
+ outputs->push_back(CONST_HDMI);
+}
+
+bool CWinSystemTVOS::MessagePump()
+{
+ return m_winEvents->MessagePump();
+}